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

peipkg

Technical Reference Manual — the package manager: repositories and trust, dependency resolution, installation, transactions and rollback, roles and claims, and package production.

Single-page view · as markdown

1.1 Overview

Peios / Peios technical reference / peipkg / Introduction

peipkg is the package manager of Peios: the program that fetches software from a repository, verifies it, and installs it onto a system. It is the consumer side of the package format and repository protocol (PSPU §5), and it is one of several programs built around that format.

1.1.1 What it does #

peipkg maintains a picture of what is installed, resolves what a requested change implies, and applies that change as a single transaction that either lands completely or does not land at all. Around that core sit the parts that make the core trustworthy: a per-repository trust state, a verification pipeline that runs to completion before any byte reaches its destination, and a journal that survives a power cut mid-write.

1.1.2 What is unusual about it #

It holds no identity. peipkg is not a daemon and has no service principal. It runs as whoever invoked it, and every file it creates or replaces is checked by the kernel against that caller's token. There is no standing privileged process to compromise, and the blast radius of a malicious package is exactly the authority of the person who installed it. The consequence runs both ways: peipkg cannot let a low-authority operator install something they could not have written by hand, and it does not need to be trusted to keep its own hands clean.

Packages carry no permissions. Every entry in a package is mode 0777, owned by uid 0, with no extended attributes. That is honest signalling rather than laxity: a mode bit in a package would imply a contract the kernel does not consult. What access control an installed file ends up with is decided at install time, from the parent directory's inheritable descriptor or from an explicit override the package declares and the operator approves.

There are no install scripts. A package cannot ship code that runs at install time. It can declare that one of three standard maintenance operations is required, from a closed enumerated set, and peipkg invokes that operation itself, from a fixed absolute path, with a cleared environment. Everything a package might otherwise want a script for — registering a service, seeding registry state — belongs to the higher-level artifacts that compose packages.

Installation targets a named root, not a path. The default root is the system root, but a system may define others — an initramfs image is the motivating case — and a package's dependency closure flows into the root the package occupies unless a dependency names a different one. A package never names a filesystem location; it names a root, and where that root lives is the installing system's business.

Several packages may contend for one filesystem name. A role is a virtual name that more than one installed package can provide, with at most one holding it. The holder's file answers the contended path through a symlink peipkg owns. Two registry daemons can be installed at once; only one is /usr/sbin/registryd.

1.1.3 The shape of an operation #

Every install, upgrade, and uninstall follows the same arc. peipkg acquires an exclusive lock, resolves the request against the installed set and the configured repositories' indexes, presents the resulting plan for confirmation, fetches and fully verifies every package the plan names, stages each file beside its destination, journals its intent, renames everything into place, commits the database, and only then runs any side effects.

The database commit is the single durability boundary. Before it, a crash rolls the whole transaction back from the journal's record of where each displaced file was moved to. After it, the transaction happened and there is only cleanup left. There is no state in which a transaction is half committed.

1.2 What This Manual Covers

Peios / Peios technical reference / peipkg / Introduction

This manual describes peipkg as it is built: the package manager, the producer toolchain that feeds it, the image composer that shares its machinery, and the repository publisher.

1.2.1 Covered here #

  • The programs and where their state lives (chapter 2)
  • Repository configuration, trust, and refresh (chapter 3)
  • Dependency resolution: satisfaction, candidate selection, and failure (chapter 4)
  • Installation: validation, staging, extraction, and registration (chapter 5)
  • Upgrade and removal, and how configuration files survive them (chapter 6)
  • Transactions: the lock, the journal, the commit, and crash recovery (chapter 7)
  • Rollback and recovery from an interrupted or failed operation (chapter 8)
  • Roles and claims (chapter 9)
  • Installation roots and image composition (chapter 10)
  • Side effects (chapter 11)
  • Producing packages with pekit (chapter 12)
  • The security model: privilege, audit, and operator authorisation (chapter 13)
  • What goes wrong and what it looks like (chapter 14)

1.2.2 Covered elsewhere #

The package format and the repository protocol are specified in PSPU §5 and are not restated here. Anything a third party has to reproduce exactly — the container layout, the manifest schema, version comparison, the payload rules, the signature construction, the index schemas, the freshness rules — lives there. This manual describes what peipkg does with those artifacts, and cites the specification rather than paraphrasing it.

Security descriptors and the access-check model belong to the kernel's access-control subsystem and are documented with it. peipkg supplies descriptor bytes at file-creation time and never interprets them.

Roles, role features, core features, and applets are separate subsystems that reference packages. A package is a distribution primitive beneath them.

Service definitions, registry seeds, and reconciller manifests are integration metadata belonging to the artifacts that compose packages, not to packages.

1.3 Terminology

Peios / Peios technical reference / peipkg / Introduction

Terms defined in PSPU §5.2 — package, manifest, files manifest, payload, repository, descriptor, index, virtual name, role, claim, holder, installation root, trust anchor — carry the same meaning here and are not redefined.

Terms specific to the implementation:

  • Transaction — the atomic unit of work. Every install, upgrade, uninstall, grant, and revoke executes within one, even when it contains a single operation.

  • Plan — the resolver's output: an ordered list of operations that, applied to the installed set, satisfies the request. A plan is computed entirely from index data, before anything is fetched.

  • Goal (or target) — one requested operation the operator named: install this, upgrade that, remove the other.

  • Candidate — an available package, drawn from a repository index, that the resolver may select.

  • World — the resolver's working model, keyed by (name, root): every installed package plus every operation in flight.

  • Journal — the record of a transaction's intent, stored as rows in the package database. It carries the backup map: for each displaced file, the sibling path its original was renamed to.

  • Staged file — a file written to a temporary sibling of its destination, within the same directory, before the transaction commits. Nothing appears at a final install path until the apply phase.

  • Backup — a displaced original, renamed aside within its own directory. Backups cost no additional disk space and are produced by a single rename.

  • Authorization — a resolver output demanding an explicit, action-specific act from the operator before the plan may be applied. Distinct from a notice, which is informational and never blocks.

  • Adoption — recording an existing unowned file as owned by an installing package, without rewriting it, when its content already matches what would have been installed.

  • Side effect — one of the three standard maintenance operations a package may declare (PSPU §5.24).

  • Recipe — a pekit.toml file plus its build script: the input to the producer toolchain describing how to turn an upstream source tree into one or more packages.

  • Lock (in composition) — the pinned, resolved closure an image composition records, so that the same inputs produce the same image. Not to be confused with the transaction lock, which is a mutual exclusion primitive.

1.4 Compatibility

Peios / Peios technical reference / peipkg / Introduction

1.4.1 Format and protocol version #

peipkg implements schema_version 1 of every document in PSPU §5: the manifest, the files manifest, the signature envelope, the repository descriptor, and both indexes. It rejects any other value, and rejects any value outside a closed enumeration — a side-effect identifier, a hash or signature algorithm, a key status, an index kind, a signature policy — rather than ignoring it.

Unknown fields are ignored everywhere except the signature envelope, which is parsed strictly.

1.4.2 Architectures #

x86_64 is the primary target and the one the system is built and tested for. aarch64 is recognised and is a secondary target.

Architecture identifiers are validated for format wherever they appear, but membership of the canonical set is not itself checked: a package declaring an architecture peipkg has never heard of parses cleanly and is simply not installable, because it matches neither the system's primary architecture nor noarch. This is the behaviour a future architecture addition needs, and it means an unrecognised architecture produces a resolution failure rather than a parse failure.

The system's primary architecture is recorded in the package database at first use. When no value is recorded, peipkg derives one from the architecture it was itself built for.

1.4.3 Multi-architecture #

Only one architecture's packages may be installed on a system at a time, alongside noarch packages. The architecture triplet convention (PSPU §5.15) applies regardless, so that today's packages stay compatible with a future extension that lifts the restriction.

1.4.4 The producer toolchain #

pekit is the producer. It builds through the same packing library peipkg reads with, so a package that packs has already been decoded by the consumer's own validators. Recipes are versioned by convention rather than by a schema field: a tool tolerates a top-level section it does not own and rejects an unknown key within a section it does.

1.4.5 Interoperability #

A package is a Zstandard-compressed pax tarball and can be inspected with ordinary tools. Extracting one on a non-Peios host yields world-writable files, because every entry is mode 0777 by design (PSPU §5.16); applying sensible host-native permissions afterwards is the extracting tool's job.

A repository is static files over HTTP. Anything that can serve a directory tree can host one, and anything that can fetch a URL and verify an Ed25519 signature can consume one.

2.1 peipkg

Peios / Peios technical reference / peipkg / The Tools

peipkg is the consumer: the program an operator runs to change what is installed on a system.

2.1.1 Verbs #

VerbEffect
installAdd packages, resolving and installing their dependency closure
upgradeMove installed packages to newer versions; with no name, every installed package
downgradeMove a package to an older version, requiring explicit authorisation
uninstallRemove packages, cascading to or refusing on dependents
undoRevert the effect of a previous transaction
claimInspect, grant, or revoke the holder of a role
repoAdd, list, remove, and refresh repositories
query, list, info, ownsRead the package database
verifyRe-hash installed files against what was recorded at install
recoverResolve an interrupted transaction
cleanGarbage-collect the index cache

install also accepts a local package file rather than a repository name. A local file has no originating repository and therefore no trust set to verify its signature against; it is accepted on the operator's say-so and its format is validated in full, but its authenticity is not established.

2.1.2 Flags that change what a transaction is allowed to do #

FlagEffect
--yesConfirm the routine "apply this plan?" prompt
--cascadeOn uninstall, remove dependents rather than refusing
--allow-staleProceed despite a repository's trust state exceeding its maximum age
--claim, --claim-all, --no-claimChange which roles an install claims (§9.4)
--dangerously-bypass-path-restrictionsPermit an out-of-layout payload from a package that declares itself a special system package

--yes confirms the routine prompt and nothing else. Every elevated action — a downgrade, a foreign replaces, a low-trust provider filling a high-trust role — raises a distinct authorization that --yes does not satisfy and that is confirmed on its own terms (§13.4). The same holds for a configuration file an uninstall would delete that has been modified since install: it is put to the operator per file — remove, keep, or abort — and with no answer on the input the uninstall aborts (§6.6).

2.1.3 Run by a program #

peipkg --driven <verb> is for a program, such as Package Manager, in place of a person: the plan, each question, progress and the outcome are JSON Lines on standard output, and the answers are read from standard input. It asks the same questions and refuses --yes. See The Driven Mode.

The query verbs (list, info, files, owns, search, history, repo list, root list) take --json and print one JSON document instead.

2.1.4 Exit behaviour #

A refused plan, a failed verification, and a rolled-back transaction all exit non-zero and name the condition. A committed transaction whose side effects failed exits zero with a warning: the packages installed, and a stale cache is recoverable by re-invocation (§11.3).

2.2 pekit

Peios / Peios technical reference / peipkg / The Tools

pekit is the producer: the build tool that turns an upstream source tree into one or more package files. It is described in full in chapter 12.

For the purposes of this chapter, three facts matter.

pekit builds through the consumer's own decoder. Packing runs the manifest it just generated back through the consumer's validators, so a package that packs successfully has already satisfied the rules a consumer applies on the way in. A recipe error therefore often surfaces as a manifest error at pack time rather than as a recipe error at validation time.

pekit's own version model is not the package version model. pekit tracks upstream releases — git tags, directory listings — and orders them by its own rules, which exist to answer "is there something newer upstream". Package versions are compared by PSPU §5.6. The two are separate, and where a recipe's template variables expose version components they come from pekit's model.

pekit signs. A package is signed at pack time with the keyring's signing.package_key, and the signature entry is the last thing written into the archive before compression.

2.3 peipkg-repo

Peios / Peios technical reference / peipkg / The Tools

peipkg-repo is the publisher: it maintains the static file tree a repository serves.

2.3.1 Verbs #

VerbEffect
initEstablish a new repository tree, with a descriptor and empty indexes
publishIngest one or more package files, derive both indexes, and sign everything
verifyCheck a published tree for internal consistency

2.3.2 What publishing does #

Publishing verifies each incoming package in full — archive structure, manifest, files manifest, per-file hashes, and its signature against the repository's own keys — before it derives anything from it. Index entries are then extracted directly from each verified manifest, never from operator input.

Both indexes are rewritten on every publication and both are stamped with the same index_version and generated_at. The new version is one greater than the highest either index carried, so the pair advances together and a consumer holding one has a usable freshness floor for the other.

2.3.3 Invariants it defends #

init refuses to run in a non-empty directory. Establishing a fresh repository at index_version 1 over an existing one would look to every consumer like an unrecoverable rollback.

publish refuses to re-publish a name, version, and architecture that already exists. That is the retention guarantee of PSPU §5.35 enforced at the point where it could be broken.

The one exception is explicit: publish --replace lets a package take the place of an archived entry with the same identity, and reports the digest it replaced. It exists for a distribution bootstrap, which builds its seed in a foreign environment and then rebuilds it natively at the same versions. It breaks retention for those versions, so it is only for a repository nobody consumes yet. Two packages with one identity in the same publish are still refused.

publish refuses a URL template that cannot distinguish one version from another, because such a template makes retention unkeepable: the second version published would overwrite the first.

Deriving the active index from the archive is a projection to the highest version per name. When two architectures of one package tie on version, the publisher stops with an error rather than choosing — a repository that silently picked one would advertise a different package than its operator intended.

2.3.4 What it does not do #

There is no verb for rotating a signing key or marking one revoked. Changing a descriptor's key list means editing repo.json and re-signing it by other means.

Publishing does not check a package's payload against the install-layout rules. A package whose payload lands outside the permitted destinations can be published and served; every consumer refuses it at install time instead.

2.4 peipkg-compose

Peios / Peios technical reference / peipkg / The Tools

peipkg-compose builds a filesystem tree from packages, without installing anything onto the machine doing the building. It is how an image is assembled.

It shares peipkg's resolver, its archive reader, its claim logic, and its package database schema. What it does not share is the runtime: it has no transaction journal to recover, no lock to hold against a running system, and no operator sitting in front of it.

Composition runs in two phases, and they can be run separately.

Resolve performs the full repository trust ceremony, resolves the requested package set against the configured repositories' indexes, and writes a lock: the pinned closure, with each package's URL, hash and declared sizes, and the trust state of every repository it draws from.

The source scan includes authenticated archive indexes when either a manifest constraint or a dependency declared by an active or local candidate can need historical versions. An unconstrained request for a development package can therefore resolve the older runtime revision it requires exactly. Shared scans check all candidates because a later lock can select a different closure from the same source universe; the scan remains immutable between locks. Repositories may omit archive indexes, but a dependency requiring an unavailable historical version still fails resolution.

Build reads the lock, fetches each package, checks its bytes against the hash the lock recorded, verifies its signature against the trust state the lock carries for its source, and assembles the tree — extracting payload, materialising claim links, and seeding a package database so that the resulting image knows what it contains.

Chapter 10 describes composition in detail, including what it does not do that an installed system's peipkg would.

2.5 The Package Database

Peios / Peios technical reference / peipkg / The Tools

peipkg's entire persistent state is one database. It is a transactional store — SQLite in write-ahead-logging mode — and that choice is load-bearing rather than incidental.

2.5.1 Why it is a real database #

Three of this manual's guarantees rest on it.

The commit is atomic. A transaction's new installed state and the closing of its journal entry are written in one database transaction, so the transaction is committed or it is not. There is no window in which it is partly committed, which is why recovery never has to finish a half-done commit (§7.8).

Reads see a consistent snapshot. A query beginning at some moment sees committed state as of that moment, regardless of a write committing underneath it. This is what lets a read-only query run without taking the transaction lock at all.

Constraints are enforced by the schema. The rule that two packages cannot own the same non-directory path is a partial unique index, not an application check — so it holds even against a code path that forgot to look.

peipkg refuses to open a database that is not in write-ahead-logging mode, rather than proceeding with weaker guarantees than it documents.

2.5.2 What it holds #

ContentPurpose
Installed packagesName, version, architecture, originating repository, install time, and the stored manifest
Owned filesOne row per path a package owns, with its type and the hash recorded at install
RepositoriesBase URL, trust keys with their statuses, priority, signature policy, and the recorded freshness floor
Role holdersWhich package holds each role
Claim linksWhich links have been materialised, and for which role and slot
TransactionsThe journal: pending and completed transactions, their operations, and the backup map
Machine metadataThe system's primary architecture, and the registered installation roots

The journal is rows in this database rather than a separate file with a separate format. Recording intent and committing are ordinary database writes, and the journal inherits the database's transactional guarantees.

2.5.3 Protection #

The database is stored under a security descriptor granting write access to the tier of principals permitted to install packages on the system. That descriptor is the journal's integrity protection: a principal outside the tier cannot forge a journal entry, and a principal inside it already holds installation authority, so a write from within is not an escalation.

The staging area is stored under the same descriptor.

Note

An earlier design signed each commit record with a key bound to a dedicated package-manager principal. peipkg holds no principal of its own (§13.1), so there is no key to sign with; the descriptor on the database is the protection instead. Narrowing that descriptor so the journal is writable only through the package-manager executable becomes possible once an elevated-executable mechanism exists, and would be a tightening rather than a correction.

2.6 On-Disk State

Peios / Peios technical reference / peipkg / The Tools

peipkg keeps four kinds of state, in three places.

2.6.1 The package database #

One transactional store, holding everything in §2.5. It is the authoritative record of what is installed.

2.6.2 Repository configuration #

One file per repository, in a configuration directory. Each declares the repository's base URL, its trust anchors, its signature policy, its priority, and two tuning values: a minimum acceptable index version, and a maximum trusted age.

The configuration file is the operator's; the recorded trust state — the verified descriptor, its keys and statuses, and the freshness floor — lives in the database. Adding a repository writes both. Removing one deletes both, and leaves installed packages alone.

Configuration is read with no authorisation check of its own. The security descriptor on the configuration directory is what decides who may change a repository's transport policy or its trust anchors.

2.6.3 The index cache #

Fetched indexes and their signatures are cached, content-addressed, with a small pointer file naming the current object for each repository. Caching avoids re-parsing; it does not avoid re-verifying. Every operation that relies on a cached index verifies its signature again, and cross-checks its version and generation timestamp against the freshness floor recorded in the database.

peipkg clean removes cache objects no pointer references.

2.6.4 Staged files and backups #

Neither is a separate directory. A staged file is written as a sibling of its destination, in the destination's own directory, under a name carrying the transaction identifier; a backup is a displaced original renamed to a sibling under a similar name.

Both choices follow from the same requirement: the rename that commits a file is intra-directory, so that it is atomic and cannot fail with EXDEV because the staging area is on a different filesystem. Backups additionally cost no disk space, because nothing is copied.

Where a destination's basename is long enough that adding the marker would exceed the filesystem's name limit, the basename is truncated to fit.

2.7 The Driven Mode

Peios / Peios technical reference / peipkg / The Tools

The driven mode is for a program that runs peipkg in place of a person at a terminal. Package Manager uses it. It changes the channel, not what is asked: every question a terminal run asks is still asked, each authorisation is still separate and audited, and --yes is refused.

Pass --driven before the verb:

peipkg --driven install org.gnu.make
peipkg --driven upgrade --dry-run

It works with every verb. It matters for the verbs that change things: install, upgrade, downgrade, uninstall, undo and claim.

--driven is not the same as the query commands' --json. --json prints one JSON document and reads nothing. --driven is a conversation that lasts the whole run.

2.7.1 Events #

Standard output carries one JSON object per line. Each has an event member naming its kind.

EventMembersMeaning
planoperations, authorisations, notes, other_rootsThe resolved plan, before anything is asked
questionid, kind, and members by kindSomething to answer on standard input
progressphase, step, steps, packageThe transaction has reached a step
warningtextWhat a terminal run prints as peipkg: warning:
messagetextAnything else a terminal run would print
donesummary, transactionTerminal: the command finished
cancelledreasonTerminal: an answer refused, so nothing was changed
errorcode, messageTerminal: the command failed

Every run ends with exactly one terminal event. Read the outcome from it, not from the exit status.

2.7.1.1 The plan #

Each entry of operations has:

  • kind: install, upgrade, downgrade or remove;
  • name;
  • from and to, the versions, where they apply;
  • repository, and local (true for a package given as a file);
  • size_download and size_installed, in bytes, for a package being brought in;
  • root, only for an operation in a root other than the one invoked (§10.3).

authorisations lists the elevated actions, as text. Each is also asked as its own question. notes lists the resolver's notices, such as a request satisfied through a provides. other_roots lists any other root the plan changes.

A plan with no operations ends with done, summary nothing to do. A --dry-run ends with done, summary dry run, after the plan. So peipkg --driven upgrade --dry-run gives the available updates.

2.7.1.2 Questions #

A question has an id, counting from 1 within the run, and a kind.

KindMembersAnswers
authorisetext, the elevated actionyes or no
proceednoneyes or no
modifiedpackage, pathremove, keep or abort

The authorisations come first, one per elevated action (§13.4), then proceed. A modified question arrives later, during staging: an uninstall would delete a configuration file that has changed since it was installed (§6.6). The conversation therefore lasts until the terminal event.

2.7.1.3 Progress #

phase is one of these, in order:

PhaseStep
fetchOne per package brought in: fetched and verified
stageOne per package brought in: its content written beside its destinations
applyEvery change moved into place
commitThe new state recorded; past this, the transaction stands
finishBackups discarded, directories reclaimed, side effects run

step counts from 1 to steps within the phase. package is present for fetch and stage. A cross-root transaction reports each root's phases in turn.

2.7.2 Answers #

Write each answer as one JSON object on a line:

{"id": 1, "answer": "yes"}

The id must be the question's. An answer with another id, or one that cannot be read, is refused, with a warning saying so. The end of input is a refusal too, as it is at a terminal: a closed input never approves anything.

2.7.3 Error codes #

CodeMeaningThe way on
staleA repository's trust state or metadata is past its maximum age, and refreshing did not cure it (§3)Run again with --allow-stale, which is audited
busyAnother package operation holds the lockTry again once it finishes
deniedThe operator may not change what the transaction touches (§13.1)None from this account
unownedA file that belongs to no package would be replaced (§5)Move the conflicting file aside and retry. For install only, --overwrite-unowned also permits replacement and keeps the displaced copy
alternate-upgradeA package is upgraded by some other means (§6)Run again with --bypass-alternate-upgrade
unresolvableNo plan satisfies the request (§4)Change the request
untrustedA configured repository's trust ceremony has never run, as with one an image ships (§3)peipkg repo add <name> runs it, or remove the repository
failedAnything elseRead message

message is the text a terminal run prints, for showing to a person. Act on code.

2.7.4 Cancelling #

Before proceed is answered, refusing any question cancels and changes nothing. Once it is answered, the transaction runs to its end.

If the driving program goes away, the transaction still finishes: peipkg ignores the broken pipe, and a question still to come reads the end of input, a refusal. A modified question refused this way aborts the transaction, which rolls back. Killing peipkg itself mid-transaction leaves a pending journal, which the next run rolls back before doing anything else (§8).

2.8 feat client interface

Peios / Peios technical reference / peipkg / The Tools

feat is a separate feature tool, not a package transaction verb. This reference keeps its client-facing state and protocol details together with the peipkg driven protocol it uses. For human-operated commands and interrupted-feature recovery, see Using feat.

2.8.1 Where its state is kept #

A feature's definition is read-only, in /libexec/features/<name>/. Its state is the registry's, under Machine\System\Features\<name>, in the value State:

StateMeans
0Not installed
1Installing (interrupted, if nothing is running)
5Installed
6Turning on
10On
9Turning off
4Removing

Changing a feature needs write access there. As shipped, only Administrators have it; everyone can read it, so anyone can see what is set up.

2.8.2 For a program #

2.8.2.1 --json #

feat list --json answers with a JSON array, one object a feature, and feat info <name> --json with one object:

MemberIs
nameThe feature's name, its directory under /libexec/features/.
title, descriptionWhat it says it is, from its feature.toml, or null. A description's paragraphs are separated by a blank line, with no other line breaks.
stateAs feat list writes it.
definedfalse for a feature whose definition is gone, known only by the state the registry still holds.
phasesThe scripts it has: install, enable, disable, uninstall.
may_changeWhether you may change its state, asked of the registry (see below).
metadata_problemPresent only when its feature.toml couldn't be read, saying why. The feature is still listed.

may_change is found by opening, for writing, the registry key a change would write: the feature's own key, or where that isn't made yet, the key it would be made in. Nothing is written. It says nothing of what the feature's scripts may do, which they find out as they run.

2.8.2.2 --driven #

feat --driven <command> <name> makes a change and reports it as JSON Lines on standard output, one event a line, for a program running feat on someone's behalf (Feature Manager is one). The events are the part of peipkg's driven mode that applies; feat asks nothing, so it reads nothing, and a script's standard input is empty.

EventMembersMeans
progressphase, step, stepsA script is starting: phase is install, enable, disable or uninstall, step of the steps this change will run.
messagetextA line a script wrote, on its standard output or error, or what feat says it did.
donesummary, stateThe change is made, or needed nothing; state is the feature's now.
errorcode, messageThe change failed.

Exactly one done or error ends the run. error's code is one of usage, not-found, denied, state, script (a script failed; its output came before, as messages) and failed.

A program that goes away mid-change doesn't stop it: feat carries on and the scripts run to their end. list and info aren't changes, so --driven refuses them with a usage error; ask them with --json.

3.1 Configuration

Peios / Peios technical reference / peipkg / Repositories

A repository is configured by a file naming its base URL and the policy peipkg applies to it.

SettingMeaning
Base URLWhere the descriptor and everything it points at are served from
Trust anchorsExpected key fingerprints, supplied out of band
Signature policyrequired or optional (PSPU §5.37)
PriorityA positive integer; lower is higher priority
Minimum index versionThe out-of-band freshness floor applied at first add
Maximum trusted ageHow long a repository's trust state stays usable without a refresh
Insecure transportWhether a non-HTTPS base URL is permitted for this repository
Allow SD overridesWhether packages from here may declare security descriptors (PSPU §5.20)

The default signature policy for a new repository is required. The default maximum trusted age is 30 days. The default priority is the same for every repository, including the official one, so the ordering the resolver applies is whatever the operator configured rather than something peipkg assumes.

An explicit maximum trusted age of zero is rejected rather than treated as "use the default", so that a mistyped value cannot silently disable the freshness check.

3.1.1 The local handle #

A repository has a name in the descriptor and a handle in the local configuration. They are conventionally the same, and nothing requires them to be: peipkg identifies a repository internally by the local handle, and compares an index's declared repository name against the descriptor's name, which it records when it verifies the descriptor.

Comparing against the handle instead made a configuration whose handle differed from the descriptor's name add successfully, write a valid cache, and then fail its cached-index check on every later operation — permanently, and with the repository dropped from resolution.

A repository recorded before the descriptor name was kept — or one in unsigned mode, whose descriptor was never verified — has none to compare against, and falls back to the handle it was recorded under.

3.1.2 Transport #

A base URL is HTTPS unless the repository's insecure-transport setting permits otherwise. The setting is per-repository; there is no global form. peipkg warns on each add, refresh and use of the cached index for package planning while this allowance is enabled.

file:// base URLs are accepted for local development. They are exempt from the transport check rather than gated by it, so a repository on removable or network-mounted media is added without the operator acknowledging the transport.

Changing the insecure-transport setting after a repository has been added means editing its configuration file. There is no verb for it, and so no prompt and no audit event accompanies the change.

3.1.3 Allowing security-descriptor overrides #

A package may declare a security descriptor for an entry it ships, and the format cannot check that its producer had any authority over the principals that descriptor grants access to. §5.20 makes that the consumer's judgement, so it is configured per repository.

It defaults to off, and a package carrying overrides from a repository that has not been vouched for is refused outright rather than installed with the overrides dropped. A repository whose packages legitimately ship descriptors — the system's own, above all, since fsbase is what declares the descriptors for /home and /tmp — must say so:

allow_sd_overrides = true

The default fails towards less authority, which is the direction that shows: an install stops and says why. Failing the other way would let a repository nobody had thought about set access control on the machine, and nothing would report it.

3.2 Adding a Repository

Peios / Peios technical reference / peipkg / Repositories

Adding a repository is the trust ceremony of PSPU §5.37: the moment an operator decides that a particular key speaks for a particular URL.

3.2.1 The ceremony #

peipkg fetches the descriptor and its detached signature, fetches the public key for each anchor fingerprint the operator supplied, checks each fetched key against the fingerprint that named it, and verifies the descriptor's signature against those anchor keys and only those. On success it records the descriptor's full key list, with statuses, as the repository's trust state.

If none of the fetched keys matches a configured anchor, the add is refused. The error lists both the configured anchor fingerprints and the fetched key fingerprints in four-character groups. Fetched fingerprints are labelled not yet trusted: this diagnostic helps compare a transcription against an independently obtained anchor; it is not a reason to adopt a key supplied by the repository itself. No trust state or index cache is recorded on this mismatch.

A matching anchor must still verify the descriptor's signature. On the success path, peipkg does not yet display a fingerprint comparison or prompt for confirmation before recording trust.

3.2.2 Two forms #

peipkg repo add <name> <url> --anchor <fingerprint> is the interactive form: everything comes from the command line.

peipkg repo add <name> is the configured form: the URL, the anchors, and the policy are already on disk, placed there by an image or a configuration manager, and the command performs the ceremony against them. When the ceremony fails for a repository whose configuration file already existed, that file is deliberately left in place — the operator put it there, and deleting it would discard their configuration over a transient network failure.

3.2.3 Bootstrapping the freshness floor #

The first index a repository serves establishes the floor that §3.4's rollback protection enforces from then on. A repository that publishes a minimum acceptable index version alongside its anchors lets peipkg refuse an add whose first index falls below it; without one the floor is whatever the first fetch returned.

Adding a repository that is already configured does not reset the recorded floor. The configured form of the command needs no arguments and reads as idempotent, so a convergence loop or a re-provisioning script runs it routinely; it re-runs the anchor ceremony and adopts the served descriptor's trust state, but the served index passes the same freshness gate a refresh applies (§3.4). An index below the recorded floor is refused, and one exactly at it counts as no progress, so the last-refresh time does not advance either.

Removing a repository is the only route that clears a floor, and it clears the whole recorded trust state with it. That is what makes lowering a floor an explicit operator act rather than the side effect of a command that looks like a no-op.

3.2.4 Fetching keys before verifying #

The descriptor names the URLs its keys are published at, so peipkg reads an unverified document to know where to fetch from. It fetches every key the descriptor declares, not only those matching the supplied anchors.

3.2.5 Official anchors #

The anchors for the official repository come from a file installed by the base system, outside any package. Bootstrap trust is a property of the image rather than of the package format: peipkg relies on the anchors being present when it first runs.

3.3 Priority

Peios / Peios technical reference / peipkg / Repositories

Every configured repository carries a numeric priority. A lower number is a higher priority.

Priority decides two things: which repository's candidate the resolver prefers when several satisfy the same dependency (§4.3), and which of two repositories counts as the more trusted when a cross-repository guard fires (§3.7).

3.3.1 What priority is not #

peipkg has no notion of an "official" repository as a distinct kind. Wherever the format's rules speak of a non-official repository acting against an official one, peipkg substitutes a comparison of numeric priorities. The two coincide exactly when the official repository has been given the lowest number — which is the recommended configuration but not something peipkg enforces, since every repository including the official one is created at the same default priority.

3.3.2 Local files #

A package supplied as a local file rather than fetched from a repository carries an empty repository name and priority zero. Zero is numerically the highest priority available, so a local file outranks every configured repository in candidate selection, and the same-repository preference of §4.3 is permanently inert for it.

3.4 Refresh

Peios / Peios technical reference / peipkg / Repositories

A refresh brings a repository's recorded trust state and cached index up to date.

3.4.1 The sequence #

peipkg fetches the current descriptor and its signature and verifies the signature against any key that was active or transitioning in the previously trusted descriptor — not against the new descriptor's own keys, which would make the update self-certifying. On success it records the new descriptor, replacing the previous key set, then fetches the active index and verifies it against the new keys.

A failed refresh leaves the previous trust state entirely intact and is reported. peipkg does not fall back to unverified state, and does not silently proceed on a stale cache.

3.4.2 The freshness gate #

An index that verifies is not necessarily current, so a refresh applies the rollback and freeze checks of PSPU §5.34 before accepting it.

  • An index whose version is below the recorded floor is rejected, even though it is correctly signed by a still-trusted key.
  • An index whose generation timestamp precedes the recorded one is rejected.
  • An index whose version and timestamp both equal the recorded values is treated as no progress: the fetch succeeded, but the last-successful-refresh timestamp is deliberately not advanced.

That third case is the anti-freeze rule, and it is the one that makes the maximum-trusted-age check below meaningful. An attacker serving the same signed index indefinitely does not get to keep a consumer's clock ticking forward.

The same floor applies to the archive index. A repository publishes both indexes at one index_version and one generation timestamp, precisely because a consumer records one floor per repository rather than one per index, so an archive index below the floor is a replay and is refused. Fetching an archive index at exactly the recorded floor is ordinary — that is the current archive — and it never advances the floor, which the active index owns.

Without this check an attacker who can substitute archive.json replays an old signed revision, and the archive index is the candidate source for every downgrade and pin: a request for a fixed version finds no candidate, and the operator is steered back onto a version that is still listed.

3.4.3 Maximum trusted age #

peipkg records the time of the last successful refresh per repository. When that exceeds the repository's maximum trusted age, an install, upgrade, or downgrade against that repository first attempts a refresh. If the attempt fails — or succeeds without progress — the operation is refused unless the operator supplies --allow-stale, which is an elevated authorisation of its own and is audited (§13.4).

Uninstall and undo are deliberately not gated. Removing something, and reverting a change, are exactly the operations an operator needs while offline or while a repository is compromised.

A configured age above 180 days produces a warning on every operation, so that a configuration effectively disabling the check stays visible.

3.4.4 What is not checked #

peipkg gates on how long since it last refreshed. It does not additionally gate on how old the index itself says it is. A repository that increments its index version on every publication while stamping an ancient generation timestamp satisfies the refresh check indefinitely.

3.5 The Index Cache

Peios / Peios technical reference / peipkg / Repositories

Indexes change when a repository publishes; peipkg reads them on every operation. The gap between those two rates is what the cache exists for.

3.5.1 Structure #

A fetched index and its signature are stored as content-addressed objects, with a small pointer file naming the current object for each repository. An older sidecar layout is still read, so a cache written by an earlier version stays usable.

peipkg clean removes objects no pointer references.

3.5.2 Re-verification #

Caching avoids re-parsing JSON. It does not avoid re-verifying signatures. Every operation that relies on a cached index verifies its detached signature again against the repository's current trust state.

peipkg additionally cross-checks a cached index against the freshness state recorded in the database, and rejects one whose index version or generation timestamp disagrees with what was recorded.

Note

The two checks close different holes. Re-verifying the signature stops substituted metadata being trusted between the cache write and the next read. The cross-check against recorded state stops an older validly signed index being dropped directly into the cache, which would otherwise bypass the refresh path where the freshness floor is enforced.

3.5.3 When the cache fails #

A cached index that fails to load or fails to verify fails the operation, naming the repository and saying to refresh it or remove it from the configuration.

Proceeding without the repository would resolve against a different set than the operator configured, and that is worse than losing candidates: a package the operator expected from the highest-trust source would come from wherever else it is available, and the cross-repository guards compare priorities against what is configured, so a repository absent from the resolution contributes nothing to them either. An escalation dressed as a warning.

3.5.4 Protection #

The cache is written with ordinary file permissions and carries no security descriptor of its own. Its integrity rests on the re-verification above rather than on who can write to it.

3.6 Transport

Peios / Peios technical reference / peipkg / Repositories

Everything a repository serves is a static file fetched over HTTP.

3.6.1 Size caps #

Every fetch is capped, so that a hostile or broken server cannot exhaust memory before anything has been verified.

ArtifactCap
Repository descriptor4 MiB
Detached signature4 KiB
Public key file64 KiB
Index64 MiB
Package filethe index-declared compressed size, plus an allowance

The package allowance is a flat 16 MiB above the declared compressed size, applied uniformly regardless of how large the package is.

3.6.2 URL resolution #

A URL in a descriptor or an index may be absolute, rooted, or document-relative, and each resolves as PSPU §5.36 describes: an absolute URL as-is, a leading-slash URL against the repository base, and anything else against the document that carried it.

3.6.3 Schemes #

HTTPS is required unless the repository's insecure-transport setting is enabled. peipkg warns for each configured repository during add, refresh and cached-index use for package planning. The warning names the repository and states that its configuration does not require HTTPS. It is emitted before the access, including when that access fails, and is a warning event in the driven interface. It describes the allowance even when a particular URL happens to use HTTPS.

file:// is supported for development and is exempt from the transport check entirely rather than gated by the insecure-transport setting.

3.6.4 Failure #

A fetch failure is reported and fails the operation. peipkg does not substitute cached data for a failed fetch without the operator saying so.

3.7 Cross-Repository Guards

Peios / Peios technical reference / peipkg / Repositories

Two relations let one repository act on another's packages, and both are gated.

3.7.1 Foreign replaces #

A replaces declared by a lower-priority repository, targeting a package originally installed from a higher-priority one, raises an authorization. The operator confirms it specifically; a general --yes does not satisfy it, and the authorising act is audited.

Note

Without the gate, a custom repository silently replacing an official-repository package is a routine upgrade away. The confirmation is what stops an escalation happening as a side effect of an operation the operator thought was maintenance.

3.7.2 Foreign conflicts #

A conflicts declared by a lower-priority repository, which would cause a higher-priority package to be removed as a cascade, is the mirror image: a denial-of-availability rather than an escalation.

peipkg resolves a conflict by rejecting the plan outright rather than by cascading removals, so the situation the guard describes does not arise: the low-trust package simply fails to install, and the high-trust one stays where it is.

3.7.3 A low-trust provider filling a role #

When the candidate that satisfies a dependency does so through provides from a lower-priority repository, while a higher-priority repository holds a name-matching package whose constraint check failed, peipkg raises an authorization and requires explicit confirmation. This is the same shape as the foreign-replaces guard: a less-trusted package taking over a name a more-trusted one was expected to fill.

The check runs when resolving a dependency. It does not run for a package the operator named directly, where it cannot fire in practice because a directly named goal carries no constraint for the higher-priority candidate to fail.

3.7.4 Origin that no longer resolves #

Each of these guards compares the priority of the repository a package came from against the priority of the repository acting on it. A package whose originating repository has since been removed — an orphan — has no configured priority to compare, so it carries a priority below every configurable value instead. Every guard that compares priorities therefore treats an orphan as at least as trusted as anything still configured, with no special case of its own.

That direction is the only safe one. Skipping the comparison, which is what an unset priority produced, meant a newly added low-trust repository could declare replaces against an orphaned ex-official package and take it over with no confirmation — so removing a repository lowered the protection on the packages it left behind. Removing a repository because its keys were stolen is exactly when that must not happen.

An orphan is not the same as a package with no origin. A raw local-file install never had a repository to lose, and is not treated as maximally trusted.

3.7.5 Orphaned packages #

list marks an orphan and names the repository that is gone; info says so in the origin line. Neither is refreshed, and no trust state stands behind either any longer.

An upgrade of an orphan is refused unless a configured repository now serves a candidate under its own name. A named upgrade errors — the alternative is reporting success having done nothing, since an unclaimed orphan produces no upgrade operation at all. An every-package upgrade warns per orphan and carries on, because one orphan must not stop the rest of the system moving.

A replaces from another package can still move an orphan aside. That is a takeover rather than an upgrade, and it is gated as an elevated action by the rule above.

4.1 Inputs and Outputs

Peios / Peios technical reference / peipkg / Resolution

Resolution is the step that turns "install this" into "do these things, in this order". It runs entirely on index data, before anything is fetched.

4.1.1 Inputs #

  • The goals: the operations the operator asked for. Install a package, upgrade one, downgrade one, or remove one.
  • The installed set: what is on the system now, from the package database, with each package's version, architecture, originating repository, and that repository's priority.
  • The available set: every package in every configured repository's index, each annotated with the repository it came from and that repository's priority.

The resolver's working model is keyed by the pair (name, root). The same package name installed in two roots is two independent entries, possibly at different versions.

4.1.2 Outputs #

Resolution produces either a plan or a rejection.

A plan is an ordered list of operations. It is partially ordered so that for every install, everything it depends on is already installed or is scheduled earlier; removals are ordered in reverse, so that a dependent is removed before the thing it depended on.

Alongside the plan, resolution emits two other things:

  • Authorizations — elevated actions the plan implies, each of which the operator confirms on its own terms before the plan is applied. A downgrade, a foreign replaces, a low-trust provider filling a role.
  • Notices — informational statements that never block. The substitution notice of §4.2 is one.

A rejection names which condition failed and which packages or constraints were involved.

4.1.3 Determinism #

The resolver is a pure function of its inputs: the same goals, installed set, and available set produce the same plan, every time. That is what makes a dry run trustworthy — the plan shown is the plan that would be applied.

Determinism holds with respect to the inputs as given, including the order candidates appear in. Where the selection rules of §4.3 leave two candidates genuinely tied, the one enumerated first wins, so a re-sorted index can change the outcome.

4.1.4 Index-only #

Resolution never downloads a package. Satisfaction checks and candidate selection use only what the index carries, which is why the index carries a package's relationships at all. Fetching is deferred until after the plan is computed and confirmed.

4.2 Satisfaction

Peios / Peios technical reference / peipkg / Resolution

A dependency is satisfied by a candidate when the conditions of PSPU §5.21 hold: the name matches directly or through provides, any constraint is met by the appropriate version, the architecture qualifier is met, and the candidate is in the dependency's root.

Three consequences of those rules shape how peipkg behaves.

4.2.1 A goal may name a role #

An install goal is satisfied under the same conditions as a dependency, with the goal's name in place of the dependency's. An operator may therefore ask to install sh, cc, or coreutils and receive whatever package provides it.

When a goal is satisfied by a candidate whose name differs from the one the operator typed, peipkg reports the substitution. This is a notice rather than an authorization: the operator is told, but an unattended run is not blocked.

Note

Reporting matters because the operator asked for one name and received a package with another. Reproducibility is not at stake — a consumer that records its resolved closure, as an image composition does, pins the chosen package, so the substitution is decided once rather than re-decided on every build.

A goal already satisfied by an installed provider is not recognised as satisfied: peipkg checks whether the goal's name is present, not whether something provides it. Asking to install a role that an installed package already provides therefore installs a second provider.

4.2.2 Upgrade and remove are name-only #

An install goal may resolve through provides. An upgrade, a downgrade, and a removal do not: they act on a package already installed under a specific name, and resolving them through provides would let an upgrade substitute a different package for the one the operator named.

4.2.3 The architecture qualifier #

The only qualifier value is any, and anything else is rejected. any means the candidate's architecture equals the depending package's effective architecture, or is noarch.

For a noarch depender the effective architecture is the system's primary architecture — a script's dependency on its interpreter resolves against the concrete system being assembled. peipkg applies that rule while selecting a candidate, not only when checking the finished plan.

Skipping the test during selection made any architecture satisfy a noarch depender, a foreign one included. That candidate entered the matching set, could win on version, was placed, and was caught only by the consistency check — which rejects the whole resolution rather than the one candidate. So a plan that a noarch candidate would have satisfied failed outright.

Candidate selection for a dependency also filters by installability now, as selection for a goal and for a name already did.

4.2.4 Concrete dependencies are resolved before capabilities #

Package names and virtual capabilities use the same dependency syntax. Peipkg therefore treats a dependency as concrete when an available package has that exact name, and resolves those dependencies before capability-only entries.

This matters when packaging derives a shared-library dependency automatically. A package may explicitly depend on its intended library package and also carry a derived dependency on that library's SONAME. Resolving the SONAME first could select another provider; resolving the explicitly named package first installs the intended provider, after which its SONAME satisfies the derived dependency. The result does not depend on the order in which those two dependencies appear in the manifest.

4.3 Candidate Selection

Peios / Peios technical reference / peipkg / Resolution

After filtering by the requested constraint and installable architectures, peipkg first selects one revision of each concrete package name. Within one name, repository preferences come first (rules 2 and 3 below), then the role version (rule 4), then the package's own version. An exact architecture match breaks a remaining version tie before repository-name ordering.

This allows a newer noarch release to replace an older x86_64 release of the same package without making an older revision win merely because of its architecture. An explicit version constraint or higher-priority repository can still select the older release.

When distinct providers remain, peipkg compares their selected revisions using rules 1 to 4 and 6 below. Rule 5 applies only to the first stage, within one concrete package name. The first rule that distinguishes two candidates decides.

Selecting the revision first also makes the result independent of catalogue order when a package's architecture changes.

  1. An exact architecture match beats noarch. A candidate whose architecture equals the system's primary architecture is preferred over a noarch candidate from a different provider.

  2. The depending package's own repository is preferred, bounded. When resolving a dependency for a package D, a candidate from D's repository is preferred over a cross-repository one — but only when D's repository is at least as trusted as the alternative. When D comes from a lower-priority repository than the cross-repository candidate, this rule does not apply and rule 3 decides.

    Note

    The bound is the point of the rule. Without it, a low-trust package pulls in low-trust transitive dependencies that shadow higher-trust alternatives, simply by virtue of being their depender.

    The rule applies when D is being installed or upgraded in this transaction. When D is already installed and is merely the reason a dependency is being resolved, peipkg does not consult the repository D was installed from, so the rule does not fire — and the same dependency can resolve to a different provider depending on whether D is being touched.

  3. Higher repository priority is preferred — a lower numeric priority (§3.3).

  4. A higher version of the name being resolved is preferred. That version is the candidate's own when it matched by name, and the matching provides entry's version when it matched through provides. A candidate matched through an unversioned provides states no version and is preferred less than any candidate that states one.

    Note

    Rule 4 compares the role, not the package. Two packages filling one role are unrelated software on unrelated version scales: asked which coreutils they offer, one project's 9.9 and another's 0.4 answer with the versions in their provides entries. Comparing those as package versions would decide the role by which project numbers its releases higher.

  5. Within one package name, a higher package version is preferred. This includes the package revision and breaks equal role-version ties during the first stage. An exact architecture match then breaks an equal package-version tie. This rule does not compare distinct providers.

  6. Ties break on the candidate's package name, by byte order, and then on its repository name.

4.3.1 Two versions of one package #

When two revisions provide the same role version, including an unversioned provides, the package's own version decides which revision represents that package. This comparison is only within one concrete package name: distinct providers have unrelated package version scales and continue to use the role version and name ordering above.

4.4 Failure Conditions

Peios / Peios technical reference / peipkg / Resolution

Resolution fails, producing no plan, when any of the following holds. Each failure names a machine-readable reason and a detail identifying the packages or constraints involved.

ConditionMeaning
UnsatisfiableA package in the candidate plan has a dependency no available package satisfies
ConflictTwo packages in the proposed resulting set trigger a conflict against each other
Architecture mismatchA package in the plan is built for neither the system's primary architecture nor noarch
Version regressionAn operation would move a package backwards without authorisation
CycleA dependency cycle the resolver cannot break by ordering
Too complexThe resolver's step budget was exhausted

4.4.1 Conflicts reject rather than cascade #

A conflict fails the plan. peipkg does not offer to remove the conflicting package to make room, which is why the cross-repository conflict guard of §3.7 has nothing to gate: a low-trust package cannot cause a high-trust one to be uninstalled as a side effect, because it cannot cause anything to be uninstalled.

4.4.2 Cycles are detected after provides resolution #

Cycle detection runs on the graph that remains once provides entries have been substituted for the dependency names they satisfy. A cycle in the raw name graph that disappears once provides is resolved is not an error.

4.4.3 Bounded work #

The forward walk carries an explicit step budget and stops with a "too complex" rejection when it is exhausted, so a pathological dependency graph cannot spin indefinitely. The algorithm itself is greedy and does not backtrack, so it is polynomial in the size of the available set regardless.

The consistency and planning passes that run after the walk are not covered by the step budget. They are polynomial too, but on a very large available set they are where the time goes.

4.5 Optional Dependencies

Peios / Peios technical reference / peipkg / Resolution

An optional dependency is never included in a plan automatically.

peipkg does not carry optional dependencies into the resolver at all: they are not part of the candidate model, so there is no path by which one could be installed without being asked for. An operator who wants one names it as a goal.

A package whose optional dependencies are absent functions correctly with reduced capability. A package that does not function without an "optional" dependency has mis-categorised it — that is a required dependency wearing the wrong label.

Optional dependencies still participate in claims. A claim path declared on an optional-dependency entry is an optional claim consumer (§9.6): the dependency need not be satisfied for the declaring package to install, and the path is materialised only if and when some eligible provider holds the role.

Note

The pattern this supports: a service that would use a log sink if one existed, and ships without one. It declares the sink's runtime path as an optional claim, installs with no provider present, and the path simply does not exist. Installing a provider later materialises the path retroactively against the declaring package's declaration; removing the provider again withdraws it, with no effect on the declaring package.

4.6 Removal Cascades

Peios / Peios technical reference / peipkg / Resolution

Removing a package that other installed packages depend on would leave the system inconsistent, so peipkg does one of two things, chosen per transaction.

Refuse is the default. The removal is rejected and the dependents that block it are named.

Cascade removes the dependents too. It is requested with --cascade, and it is the operator taking responsibility for a larger change than they typed.

Removals are ordered in reverse dependency order, so a dependent is always removed before the package it depended on.

4.6.1 Blocking relations #

A removal is blocked by an installed package that depends on the one being removed, and by an installed package whose replaces targets it. Both are computed against the state the transaction will produce, so a dependent that is itself being removed in the same transaction does not block.

4.6.2 System-critical packages #

Some packages are needed for peipkg itself, or for the system, to work: peipkg's own binary and its trust anchors, and the core system packages.

The intended guard is a refusal unless the operator supplies an operation-specific override — an --allow-critical flag — which is a foot-gun guard rather than a security boundary: under the access model the operator already holds whatever authority the underlying deletions require, so the guard exists to prevent an accidental removal disabling the system, not to deny an authorised operator who means it.

peipkg has no notion of a system-critical set, no such flag, and no guard. Uninstalling the package manager is an ordinary removal.

4.7 Roots and Resolution

Peios / Peios technical reference / peipkg / Resolution

A dependency is satisfied within a specific installation root. By default that is the same root as the depending package, so a package's closure flows into the root the package occupies. A dependency's root field overrides that, naming a different one.

4.7.1 Two resolution modes #

peipkg resolves in one of two modes.

Cross-root resolution honours a dependency's root field, placing the dependency in the root it names and producing a plan whose operations are grouped by root.

Single-root resolution treats every operation as belonging to one root. A dependency carrying a root field is placed in the depending package's root instead, and the field has no effect.

install resolves cross-root. Default upgrade also uses cross-root resolution in peipkg 8b588ae8; --no-recurse confines upgrade resolution and execution to the current root. The cross-root dependency chapter keeps that source-backed distinction separate from the earlier descriptions of downgrade, uninstall, and undo, whose paths are not verified by this upgrade correction.

4.7.2 Top-level placement #

Where an operator names a package directly with no explicit root, the package's default_root decides where it lands. A dependency's placement is never governed by the dependency's own default_root; only by the depending package's root and the dependency's root field.

4.7.3 Cascading across roots #

Default upgrade resolves the current root and its reachable named roots into one plan, with one approval. A named package is considered in each reachable root where it is installed. If the plan changes multiple roots, the coordinated executor verifies all packages, prepares the participants, then attempts commits root by root. Preparation failure attempts rollback; a commit failure can leave partially committed work needing recovery. These are not independent continue-on-error upgrade transactions.

See Named roots for operator checks and the pinned source/release scope, and transaction scope for the execution boundaries.

5.1 Preconditions

Peios / Peios technical reference / peipkg / Installation

An install begins with a package file already fetched, the repository it came from with its trust state, the current database state, and the plan that called for the install.

The following hold before the install proceeds:

  1. The package's hash matches what the repository index recorded.
  2. The package's signature, if present, verifies against the trust set scoped to its originating repository.
  3. The package is not already installed at the same version and architecture in this root.
  4. The package's architecture is the system's primary architecture or noarch.
  5. The package's dependencies are satisfied by the installed set, by the plan in flight, or by both together.
  6. No installed package conflicts with it.

A precondition failure aborts the install, and the transaction containing it is rolled back.

5.1.1 Verification before extraction #

For a transaction containing several installs or upgrades, every package's signature and index-hash verification completes before any package's payload is extracted.

Note

This prevents a class of multi-package attack: package A is verified, extracted, and its contents then influence the verification or extraction of package B — A installs a tool B's extraction invokes, or A creates a directory whose descriptor decides where B's files land. Verifying everything first means extraction operates on a known-good set of payloads.

The guarantee spans every root the operation touches. A cross-root transaction fetches and verifies every participating root's packages before preparing any of them, because a package extracted into one root is as present on the filesystem as one extracted into another — the attack above works just as well with A and B in different roots.

5.2 Validation

Peios / Peios technical reference / peipkg / Installation

Before anything is staged, the package is decompressed, parsed, and checked in full.

  1. Decompress and walk the archive, enforcing the layout and ordering rules of PSPU §5.12.
  2. Parse the manifest.
  3. Parse the files manifest.
  4. Check that every payload file has an entry in the files manifest and that every entry has a file — in both directions.
  5. Check that the manifest's name, version, and architecture match what the plan selected.
  6. Validate every payload path against PSPU §5.13 and every entry type against PSPU §5.12.
  7. Validate every symlink's target against PSPU §5.17.
  8. Validate every payload entry's destination against the permitted install destinations.

5.2.1 Destinations are checked here #

Step 8 is not a duplicate of the producer's own check. A package arriving on a target system need not have been produced by a cooperating producer, so validation performed while packing says nothing about the bytes about to be written. This is where the destination rules are actually enforced.

Ancestor directories are exempt. An archive carries an explicit entry for every ancestor of the content it ships, and those are structure rather than destination claims. A directory entry is checked only when no other payload entry sits beneath it — which is the archive shape of an explicit empty directory, and is a destination claim.

Special system packages need two keys. A package declaring special_system_package has waived the producer-side layout check. That grants nothing here: peipkg refuses an out-of-layout payload unless the operator also passed --dangerously-bypass-path-restrictions. When the declaration arrives without the flag, the refusal names the refused request, so that an operator can tell "this package asked for an exemption I did not grant" from "this package is malformed".

When both keys are present, the destination check is skipped entirely for that package, with no residual denylist. A package installed this way can write anywhere, including under /lcl/policy.

5.2.2 What is not re-checked #

The determinism rules of PSPU §5.11 constrain the archive's bytes: ordering, modification times, ownership, mode, extended attributes, and header format. peipkg checks entry ordering and nothing else. A package whose entries carry a mode other than 0777, a non-zero owner, an mtime unrelated to its manifest, or extended attributes is accepted and installed.

Because extraction ignores tar modes entirely (§5.4), such a package behaves no differently on Peios. Extracted elsewhere with an ordinary tar tool, it does not.

5.2.3 Size cross-checks #

The manifest's declared installed size has to equal the sum of the file sizes in the files manifest, and a package where the two disagree is rejected. The decompression bounds of PSPU §5.27 are enforced continuously during the walk, on every chunk of output.

The cap is computed from the size the manifest declares, plus the fixed overhead allowance, subject to the absolute 4 GiB ceiling. The index's declared sizes are parsed and carried onto the candidate but are not compared against the manifest's and are not used as the bound.

5.3 Preparation

Peios / Peios technical reference / peipkg / Installation

With the package validated, peipkg computes what the install will do: which files are created, which directories are created, which side effects will be scheduled.

5.3.1 Collisions #

A payload path already owned by another installed package is a collision, and the database refuses it: the rule that two packages may not own the same non-directory path is a partial unique index on the owned-files table.

That constraint is evaluated when the transaction's database changes are written, which is at commit — after the files have already been renamed into place. A colliding install therefore fails, but only after paying the full download, extraction, and on-disk replacement, and recovery depends on the rollback succeeding.

There is no earlier check. Collisions are not detected at plan time or at preparation time.

5.3.2 Disk space #

peipkg does not check free space before staging. Exhaustion is discovered when a write fails — during staging, where rollback is straightforward, or during the apply phase, where some files have been renamed into place and some originals are sitting at their backup paths.

A transaction may span several filesystems, since installation roots and the destinations within a root can be separate mounts, so a single global free-space figure would not be the right check even if one were made.

5.4 Extraction

Peios / Peios technical reference / peipkg / Installation

Payload entries are processed in archive order. Nothing appears at a final install path during this phase.

5.4.1 Staging #

Each regular file is written to a staged sibling of its destination: a temporary name in the destination's own directory, carrying the transaction identifier. Where a destination's basename is long enough that adding the marker would exceed the filesystem's name limit, the basename is truncated to fit.

Staging in the destination's own directory is what makes the commit-time rename intra-directory, and therefore atomic and immune to failing because the staging area is on another filesystem.

Directories are created as they are encountered, and every directory the transaction creates is recorded so that a rollback can remove it again. Symlinks are created with the linkname the tar entry carried, which was validated at parse time and so is known safe by the time extraction reaches it.

5.4.2 Modes #

Extraction ignores the tar entry's permission bits. Every file is created mode 0755, and every directory mode 0755.

This follows from the format: a package's modes are all 0777 and carry no information (PSPU §5.16), so the consumer has to choose something. What it chooses is uniform and executable.

5.4.3 Security descriptors #

peipkg creates entries without supplying an explicit security descriptor, so the kernel computes one by inheritance from the parent directory at creation time.

That is the default and it is what the overwhelming majority of entries get. Inheritance is expressed by the absence of a descriptor, not by peipkg computing one.

5.4.3.1 Overrides #

A package may declare a descriptor for a specific entry, through the manifest's sd_overrides (PSPU §3.3.5). Every override is checked before anything is installed — the descriptor decodes from base64, stays inside the size limit, and parses; the list is sorted and free of duplicates; and each path names a real payload entry that is not a symlink. §5.20 requires all of that to happen up front, because deferring it to the moment a descriptor is applied turns a malformed package into a partially completed install.

Overrides take effect before content is written. On Peios, a new file or directory receives its descriptor in the native create call. A staged filename is observable too; protecting only the final rename is insufficient.

All directory overrides in the transaction are collected and authorised before planning creates parent directories. Parent scopes are established before their children, including when a different package supplies the scope. Conflicting descriptors for the same directory are rejected. Image composition also establishes these scopes before parallel payload extraction.

A declared override on an existing directory is applied to that directory. Existing descendants are not recursively reset, and existing directories without an override retain their policy. This preserves unrelated operator customisations; changing a subtree's policy is not a migration of its children.

Off Peios, composition stamps or records image metadata before copying file content. The native creation path falls back only when the native syscall is unavailable. Access denial or an invalid descriptor fails the operation; the installer does not temporarily broaden the descriptor to let itself write.

5.4.3.2 The override policy #

The kernel checks that a declared descriptor is well-formed. Nothing checks that the package's producer had any authority over the principals it grants access to, and §5.20 makes that the consumer's question.

peipkg answers it per repository, from allow_sd_overrides in the repository's configuration. A package carrying overrides from a repository that does not permit them is refused — not installed with the overrides quietly dropped, which §5.20 forbids by name. A package declaring no overrides is unaffected: the policy gates declaring descriptors, not installing.

Every override applied is listed in the install report.

Note

This is a deliberate subset of §5.20. The spec also asks for each override to be shown to the operator with a diff against what inheritance would have produced, and for a per-repository allowlist of the principals an override may name. Both require peipkg to compute KACS inheritance itself in order to render the diff. Until a repository exists that is neither wholly trusted nor wholly refused, the per-repository switch enforces the binding rule and leaves the graduated middle unbuilt rather than half-built.

5.4.4 Hashing #

Per-file content hashes are verified during the verification pass, over the same immutable in-memory bytes that extraction then reads. The package is fully hash-checked before a single staged file is written.

Extraction itself re-checks nothing. The guarantee that what lands on disk is what was hashed rests on both passes reading the same buffer, which every current caller arranges by handing extraction a reader over already-verified bytes in memory.

5.5 Path Resolution

Peios / Peios technical reference / peipkg / Installation

An install path is resolved component by component, against a directory descriptor, never as a string handed to the kernel to walk. Each component is opened relative to the descriptor of its parent, and a component that is not a real directory — a symbolic link above all — ends the resolution with an error.

The descriptor of the destination's parent is then held from the moment the operation is planned until the commit that acts on it. Staging, displacement and installation are all performed against that one descriptor.

5.5.1 No symbolic link is ever traversed #

This holds for a link peipkg created moments earlier in the same transaction as much as for one that was already there. A well-formed package never has a payload entry whose ancestor is a symbolic link, so the refusal fires only on a malformed or hostile package, where aborting is what you want.

Symbolic links as leaf payload entries are ordinary and are created normally. What is forbidden is walking through one.

Two things follow, and only one of them is about a race.

A symbolic-link ancestor cannot redirect a write. The format's rules permit a package to ship a link whose target resolves into a permitted destination, and permit a different package to ship a file whose path descends through that link's location — each entry validates in isolation, and both are individually valid. Resolving the second package's path now stops at the link instead of following it. Without that, the file landed where the link pointed, the database recorded the archive's path, and the collision constraint of §5.3 compared the wrong name and never fired.

The check and the commit act on the same directory. The window between them is the whole transaction — a pre-existing entry is detected while the intent is journalled, and the rename that acts on it runs after every package has been downloaded, decompressed and staged. The pinned descriptor is what makes that window harmless: the commit renames within the directory it examined, not within whatever the path string resolves to by then.

5.5.2 The final component #

Exclusive-create semantics still apply to a staged file, so it cannot land on an existing name. A pre-existing entry at a destination is detected without following it and is displaced by a rename, which does not follow a link at either end — so a link sitting at an install path is renamed aside rather than written through.

5.5.3 Recovery #

Recovery has only the journal's strings; there is no descriptor to carry across a crash. It re-resolves them with the same refusing walk, so an undo lands on the paths the journal named and nowhere else. There is no in-flight window to race at that point, only a resolution to get right.

5.5.4 Implementation #

The walk is one openat per component with O_PATH|O_NOFOLLOW| O_DIRECTORY, and the commit is renameat against the pinned descriptor. O_PATH|O_NOFOLLOW alone would open the link itself; O_DIRECTORY is what turns that into the ENOTDIR the refusal rests on.

openat2 with RESOLVE_NO_SYMLINKS would fold the walk into a single syscall and add RESOLVE_BENEATH, RESOLVE_NO_XDEV and RESOLVE_NO_MAGICLINKS. It is worth having and is not needed for the guarantee: a walk that never follows a link and never accepts a .. component cannot leave the root by name.

Descriptors are cached per directory, so the count a transaction holds is the number of distinct directories it writes into, not the number of files.

Note

The format's symlink-target rules are the first layer of the defence PSPU §5.26 describes, and component-wise resolution against a pinned parent descriptor is the second. Both are in place.

5.6 Registration

Peios / Peios technical reference / peipkg / Installation

Once a package's files are staged, peipkg records what the install will mean:

  • the package's identity — name, version, architecture, and root;
  • the repository it came from;
  • the install timestamp;
  • every payload path it owns, with the type and content hash of each;
  • its manifest, stored whole, so that later operations can consult the package's own declarations without the package file.

These rows are written inside the transaction's database commit and become visible only when that commit succeeds.

5.6.1 Why the manifest is stored #

Several later operations need a package's own declarations rather than an index entry's copy of them: reconciling claims after an unrelated install, computing what a removal breaks, and re-deriving a role's claim-path set. Keeping the manifest means those operations do not depend on a repository still being configured, or still existing.

5.6.2 Ownership #

A file is owned by the package whose install created it. Ownership is what makes uninstall, peipkg owns, and the collision constraint possible, and it is recorded per path rather than per package prefix.

Directories are recorded as owned but are shared: several packages may own the same directory, and the collision constraint applies only to non-directory entries.

5.7 Pre-Existing Files

Peios / Peios technical reference / peipkg / Installation

A path may already exist on the filesystem without belonging to any installed package — left by a manual copy, inherited from a non-Peios installation, or written by something outside the package manager.

5.7.1 The unowned-file policy #

Before replacing a destination, peipkg checks its recorded owners. A path owned by the package being changed follows ordinary replacement; a path owned by another package is handled by collision checking. For an existing path with no recorded owner, unownedPolicy selects one of three outcomes:

  • Adopt matching content. A regular file must have the payload's content hash; a symlink must have the same target. A different entry kind never matches. The operation may proceed and record ownership.
  • Refuse differing content unless overwriting was explicitly authorised. The unowned error names the package and conflicting path. The existing content is preserved.
  • Displace differing content when overwriting was authorised. The existing entry is renamed aside, and its backup survives transaction commit. A warning reports the displaced file and backup name.

These behaviours are implemented for payload installation and role claims. The comparison is about entry kind and content, not equality of all metadata. Matching content permits the normal file operation; it does not promise that the existing inode is retained.

5.7.2 Recovering from a refusal #

Move the conflicting file aside and retry the operation. install also accepts --overwrite-unowned, which authorises displacement while keeping the previous content. Upgrade, downgrade, undo, remove and claim have no equivalent command-line flag; a driven client cannot retry those commands by adding it.

Ordinary replacement backups are discarded on commit (§8.2). A backup kept because an unowned file was displaced is the exception and remains available to the operator.

6.1 The File Diff

Peios / Peios technical reference / peipkg / Upgrade and Removal

An upgrade replaces one installed version of a package with another. It is conceptually a single atomic operation combining an install of the new version with an uninstall of the old, arranged so that no moment leaves the system without the package's content.

The same procedure handles a downgrade. The two differ only in which version comparison applies; the format treats them identically.

6.1.1 The four categories #

An upgrade is computed as a diff between the old version's file set, read from the package database, and the new version's, read from the new package's files manifest.

CategoryMeaningAt commit
AddedIn the new set, not the oldStaged and renamed in
ReplacedIn both, with different contentOld renamed aside, staged renamed in
UntouchedIn both, with identical content hashLeft alone
RemovedIn the old set, not the newRenamed aside

An untouched file keeps its inode: nothing is staged for it and no rename runs. It still gets an ownership row under the new version, so the upgrade's file set is complete and a later uninstall removes it.

Two conditions narrow the category, both of them cases where a file's content is not the only thing being installed at that path:

  • The file must actually be present. One the operator deleted is not untouched, and an upgrade is the right moment to put it back.
  • A path carrying a signature sidecar or a §3.3.5 descriptor override is always rewritten. Both are applied to the staged inode, so the path needs one even when its content has not moved.

6.1.2 Ordering at commit #

Added files are renamed into place. Replaced files have their original renamed aside first, then the staged file renamed in. Removed files are renamed aside.

A directory the previous version owned that the new payload does not carry is released, and reclaimed after commit if it is then unowned and empty (§6.6).

6.2 Configuration Files

Peios / Peios technical reference / peipkg / Upgrade and Removal

A package's configuration under /usr/etc/ is seed configuration: the package's defaults, not the running system's effective configuration.

6.2.1 Modified detection #

When an upgrade would replace a configuration file, peipkg compares the file's current on-disk content hash against the hash recorded for it at install.

  • Unmodified — the content matches what was recorded. The file is replaced like any other.
  • Modified — the content differs. The operator's file is left in place, the new version's default is written beside it as <name>.peipkg-new, and the divergence is surfaced in the operation report.

The check applies to paths under /usr/etc/, and also to paths under a bare /etc/ — the latter only so that packages installed before the layout moved to /usr/etc/ keep their protection. It does not permit installing to /etc/, which is a merged view rather than storage.

6.2.2 What is recorded #

On the modified branch, the file peipkg writes is the .peipkg-new sibling, but the ownership row it records names the original path and carries the new version's hash.

Two things follow. peipkg verify compares the recorded hash against what is on disk, so it reports that file as modified on every run, permanently, for a file peipkg itself deliberately preserved. And the .peipkg-new sibling is recorded nowhere, so it is owned by no package, is not removed by an uninstall, and is not attributed by peipkg owns.

6.2.3 Interaction with the missing untouched category #

Because §6.1's untouched category is not computed, a configuration file whose content is identical between the two package versions is classified as replaced. Modified detection then fires on the operator's edit, writes a .peipkg-new carrying exactly the content the package already shipped, and warns about a divergence from a version that changed nothing.

6.2.4 Where this is going #

The intended end state is the one the filesystem layout describes: runtime configuration is materialised by reconciller daemons from registry state, operators do not edit configuration by hand, and an upgrade replaces seed configuration unconditionally. Modified detection is what prevents an upgrade destroying a hand-edited file until that framework exists.

The two models converge rather than compete: once reconcillers claim a set of paths, modified detection applies only to the unclaimed remainder, and reconciller-managed seed files are replaced outright.

6.3 The Upgrade Procedure

Peios / Peios technical reference / peipkg / Upgrade and Removal

6.3.1 Preconditions #

Every install precondition (§5.1) holds for the new version, except that "not already installed" is replaced by "the installed version has the same name and architecture, in the same root, at a different version".

In addition: the new version's dependencies are satisfied, or will be by other operations in the same transaction; no installed package depends on the current version in a way the new version cannot satisfy; and a downgrade carries an explicit authorisation.

6.3.2 The steps #

  1. Validate the new package exactly as an install does (§5.2).
  2. Diff the old file list against the new (§6.1).
  3. Stage every added and replaced file to a temporary sibling of its destination, verifying its content hash.
  4. Apply, at commit: rename added files in; rename replaced originals aside and the staged files in; rename removed files aside.
  5. Schedule side effects, including those implied by files being removed as well as those the new version declares.
  6. Re-register: replace the database's record for this package with the new version's identity, file list, and manifest.

6.3.3 Dependents #

An upgrade that would leave a dependent's constraints unsatisfied cannot proceed on its own. The resolver includes the dependent's upgrade — or its removal — in the same transaction, or the resolution fails.

6.4 Replaces

Peios / Peios technical reference / peipkg / Upgrade and Removal

A replaces relation expresses supersession: this package takes over from that one, typically after a rename.

An upgrade triggered by a replaces follows the ordinary upgrade procedure with the replaced package treated as the currently installed version, even though its name differs. The database record for the replaced package is removed and a record for the replacing package is created. From the system's point of view the replaced package is uninstalled and the replacing one installed; the file diff ensures no payload file is spuriously removed during the transition.

6.4.1 The guard #

A replaces declared by a package from a lower-priority repository, targeting a package installed from a higher-priority one, requires explicit operator confirmation before it is applied (§3.7). A general --yes does not satisfy it.

The guard compares repository priorities. For a package whose originating repository has since been removed, there is no priority to compare and the guard does not fire.

6.4.2 Removal interaction #

A package cannot be uninstalled while another installed package's replaces targets it, unless that package is being removed in the same transaction.

6.5 Downgrade and Undo

Peios / Peios technical reference / peipkg / Upgrade and Removal

6.5.1 Downgrade #

A downgrade is an upgrade whose new version is older than the installed one. The procedure is identical, with one additional precondition: the operator has explicitly authorised it.

The authorisation is raised by the resolver as an elevated action, and is not satisfied by the routine confirmation prompt.

Note

Explicit authorisation is required because a downgrade is unusual: it implies the operator wants to revert to a known-good earlier state, often after a failed upgrade. Making them opt in ensures the downgrade is intentional rather than the result of a misconfigured constraint.

A downgrade target has to be available from a configured repository's archive index, or from a package file already cached. Versions pruned from an archive cannot be reached without an externally supplied package file.

6.5.2 Undo #

peipkg undo reverts the effect of a previous transaction. It works by re-resolving against the archive index with downgrades permitted, and applying the resulting plan as an ordinary transaction, rather than by restoring the previous transaction's backups.

Two consequences follow from that choice. Undo needs the archive index, and therefore a reachable repository or a warm cache — it is not an offline operation. And it produces a new transaction with its own journal and its own rollback, rather than unwinding an old one, so the guarantees are the same as any other change.

6.5.3 What a version revert does not revert #

Reverting a package's version does not revert anything outside that package's payload: registry state, configuration materialised by reconcillers, runtime data under /var/, and any user data are unaffected.

Comprehensive system rollback, including state under registry control, is a higher-level concern handled by recovery snapshots. Package-level version revert covers the common case of an update breaking something.

6.6 Uninstall

Peios / Peios technical reference / peipkg / Upgrade and Removal

6.6.1 Preconditions #

The named package is installed; no other installed package depends on it unless the plan removes them too; and no other installed package's replaces targets it.

Blocked removals cascade or refuse, per §4.6.

6.6.2 The steps #

  1. Enumerate the package's owned paths from the database.
  2. Check each non-directory path: that no other installed package also owns it (below), and — for a configuration file — that its content still matches the hash recorded at install (§6.7). A modified configuration file is put to the operator before anything is touched.
  3. Prepare the removal.
  4. Remove: rename each path aside as a backup rather than deleting it, so that the uninstall can be rolled back.
  5. Schedule side effects implied by what was removed.
  6. Deregister: delete the package's record, withdraw any role it held, and reconcile any role whose claim paths it declared (§9.7).
  7. Reclaim the directories the package owned that are now owned by no package and are empty, deepest first.

Backups are discarded when the transaction commits, except the backup of a modified configuration file whose removal the operator authorised: that one is kept beside where the file was.

6.6.3 Modified configuration files #

A configuration file — a path under /usr/etc/ or the legacy /etc/, the same scope the upgrade's modified-detection uses (§6.2) — whose content differs from the recorded hash is surfaced before the removal is prepared, and the operator chooses per file: remove it, with the previous content kept at the backup path and the authorisation written to the audit stream; keep it, in which case it stays where it is and becomes unowned, since the package's ownership row goes with the package; or abort the transaction. Anything but an explicit remove or keep aborts, end-of-input included, so --yes does not answer the question and a non-interactive uninstall of a package with a modified configuration file refuses, naming the file. Binaries, libraries and data are not hashed at uninstall.

6.6.4 Directories #

Removing the package's rows releases its ownership of its directories. After the commit, each such directory that no installed package owns any longer and that is empty is removed, deepest first, so a directory is attempted after everything it contained. A directory another package still owns, or that holds anything — an unowned file a runtime wrote, an operator's addition — is left where it is, silently: those are reasons to keep it, not failures. Only an unexpected error in removing an empty unowned directory is reported, as a warning on the operation report; the transaction has already committed.

An upgrade applies the same rule to the directories the previous version owned that the new payload does not carry (§6.1).

6.6.5 Overlapping ownership #

Two packages owning one non-directory path is prevented by the database schema, but a degraded state — a corrupted database, a manual intervention — could produce one. Each non-directory path scheduled for removal is checked against the ownership table first. A path that another installed package also owns is left in place, since the other owner still needs it, and the operation report carries a database-integrity warning naming the path and both owners, so that the operator knows the database needs inspection rather than the filesystem.

6.7 Modified Files

Peios / Peios technical reference / peipkg / Upgrade and Removal

A file whose on-disk content no longer matches the hash recorded at install has been modified since installation — by a person, by a program, or by corruption.

6.7.1 Where the check runs #

peipkg compares recorded hashes against disk in three places.

peipkg verify does it on demand, across every recorded file, and reports what differs.

An upgrade does it for configuration files, to decide whether to preserve an operator's edit (§6.2).

An uninstall does it for configuration files too, and puts each modified one to the operator before the removal is prepared: remove it with the previous content kept as a backup, keep it as an unowned file, or abort (§6.6).

Note

A modified file at removal time is either a customisation that removal destroys, or an unauthorised modification of a system file. Surfacing it is what lets the operator tell which, and the choice is theirs.

6.7.2 The cost of checking #

Hashing every installed file at uninstall is expensive: a large package on slow storage takes seconds. The check is therefore restricted to the paths where customisation is expected — the configuration scope the upgrade already uses, /usr/etc/ and the legacy /etc/ — and skipped for binaries, libraries and data. A hand-patched binary is removed without a question; peipkg verify is the tool for finding one.

7.1 Atomicity

Peios / Peios technical reference / peipkg / Transactions

Package transactions coordinate recorded package state and changes to files. That does not make all filesystem changes, every root's database, and post-commit maintenance one instantaneous switch.

For the cross-root path reviewed in peipkg 8b588ae8:

  • Packages for all participants are fetched and verified before extraction.
  • Preparing roots persists recovery data and moves files into place. Those files can become visible before a root's package-state commit.
  • Package state commits separately in each root. A commit failure can leave some roots committed and others pending; already committed roots are not undone by rolling back a sibling.
  • A preparation rollback can itself fail, leaving a pending journal and files that must not be treated as restored.

These boundaries follow the coordinated execution sequence, preparation and per-root commit, and rollback-error handling. Logical errors, power loss or other system faults therefore require checking the actual journal and files. The older blanket promises that uncommitted work is invisible and partial effects never occur do not describe this path.

7.1.1 The single durability boundary #

Within one root, updating package metadata and closing that root's journal transaction share a single database transaction. Its database commit is that root's durability boundary. It is not a global cross-root commit.

A pending cross-root journal does not by itself determine recovery direction: if none of the participants committed, recovery rolls pending work back; after a participant commits, recovery rolls pending siblings forward using persisted completion data. Missing data or another failure can prevent completion. See Crash recovery.

The source qualification here does not verify every single-root failure path or the revision shipped in an installed image. Use the operator recovery checks and visibility limits before treating a failed or interrupted operation as unchanged, restored or ready to use.

7.2 Scope

Peios / Peios technical reference / peipkg / Transactions

A transaction may contain any combination of installs, upgrades, and uninstalls on different packages.

7.2.1 Ordering #

Operations are ordered so that at commit time no operation depends on a package whose install has not been committed first. Forward operations are topologically sorted dependencies-first; removals are sorted and reversed, so a dependent is removed before what it depended on.

7.2.2 One operation per package #

A transaction cannot contain two operations affecting the same package. This is structural rather than checked: the resolver's world is keyed by (name, root) and emits at most one forward operation per key, deriving removals as the complement.

An upgrade is how a version transition is expressed. A hard reinstall is a removal and an install, in two separate transactions.

7.2.3 Cross-root transactions #

An operation touching several installation roots produces one transaction per root, sharing a cross-root identifier. Locks are acquired for every participating root, in resolved-path order, so that two concurrent cross-root operations cannot deadlock against each other.

Every package of every participating root is fetched and verified before any root is prepared. Verify-all-before-extract (§5.1) is an obligation across the whole operation rather than within one root of it: a package extracted into one root is as present on the filesystem as one extracted into another, so preparing root by root would put one root's payload in place before the next root's signatures had been looked at.

After that verification, every participant is prepared before the commit loop begins. Preparation failure aborts and attempts to reverse prepared changes; rollback errors can leave a pending journal. The commit loop then attempts each prepared root's commit, continuing those attempts after a failure and reporting a partial-commit error.

This ordering follows peipkg 8b588ae8's coordinated executor. It does not establish a globally atomic filesystem switch: preparation moves files, and post-commit maintenance for one root can run before another root's commit is attempted.

Cross-root recovery chooses rollback or roll-forward from participant state. See Named roots for operator checks and the pinned source/release scope.

7.3 The Lock

Peios / Peios technical reference / peipkg / Transactions

At most one transaction is in progress at a time in a given root.

peipkg acquires an exclusive lock before beginning. A second invocation detects the lock and fails immediately with a "transaction in progress" message rather than waiting.

7.3.1 Staleness cannot happen #

The lock is a flock(2) advisory lock on a file in the root's state directory, held by the running process. The kernel releases it when the process exits, however it exits — cleanly, by signal, or by being killed.

That makes staleness impossible by construction. There is no timeout, no liveness probe, and no process-identity comparison, because there is nothing that could hold a lock after the holder is gone.

Note

The alternative — a lock file with a recorded process identifier, a kill(pid, 0) == ESRCH liveness probe, and a timeout — is the design this avoids. A timeout-only staleness check breaks single-writer atomicity outright: a long-running but perfectly live transaction, a fifty-package install on slow storage, can be declared dead by another invocation, and both then proceed concurrently. An advisory lock is not a weaker version of that design; it is a stronger one.

A second structural guard backs it up: the database carries a partial unique index permitting at most one transaction in the pending state, so even a defect that bypassed the lock could not produce two concurrent pending transactions.

7.3.2 What the lock does not cover #

A read-only query against committed state does not take the lock. The database provides snapshot-isolated reads, so a query sees a consistent view of committed state as of the moment it began, regardless of a write committing underneath it. Listing installed packages during a long install is safe and does not block.

7.4 The Commit Procedure

Peios / Peios technical reference / peipkg / Transactions

The sequence below explains one root's package-database durability boundary. It is not the whole cross-root execution sequence. In the reviewed coordinated executor, all participants are prepared before the per-root commit loop. Files can already be visible, and another root can already be committed when one root's commit fails. See Scope and Atomicity for the pinned boundaries.

7.4.1 1. Record intent #

Before any file moves, the transaction's intent is written to the journal: the set of file operations, and for each one the staged file's path and the path its displaced original will be backed up to — the backup map. Directories the transaction will create are recorded too.

The journal is rows in the package database, so recording intent is an ordinary database write.

7.4.2 2. Apply file operations #

For each operation: rename any displaced original aside as a backup, then rename the staged file into place.

Throughout this phase every change is individually reversible from the backup map. Nothing has been deleted; a replaced file is sitting beside its own destination under a different name.

7.4.3 3. Commit #

In a single database transaction, write the new installed state — the package rows, the owned-file rows, the claim holder and link rows — and mark the journal's pending transaction committed.

This database commit is that root's durability boundary. Its package metadata and journal closure are committed together; this does not make filesystem visibility or other roots' commits atomic with it.

7.4.4 4. Invoke side effects #

Deduplicated across the whole transaction, after the durability boundary. A side-effect failure therefore cannot roll the transaction back: the transaction is already committed, side effects are idempotent, and a failed one is reported and corrected by re-invocation.

7.4.5 5. Clean up #

Discard staged files, and delete the backups.

7.4.6 What the ordering buys #

A crash before step 3 leaves the journal's transaction pending, and recovery rolls it back from the backup map. A crash after step 3 leaves it committed, and recovery has only step 5 to finish.

That describes a single-root recovery boundary, not coordinated recovery. A pending cross-root participant can instead need roll-forward after a sibling committed; rollback can also fail and remain pending. Follow the source-scoped recovery path. The atomic database commit does not establish that there is no observable intermediate filesystem or multi-root state.

7.5 The Journal

Peios / Peios technical reference / peipkg / Transactions

The transaction journal is part of the package database. A pending transaction is rows in the database store, not a separate file with a separate format.

7.5.1 What it holds #

Row kindContent
TransactionIdentifier, state, schema version, and for a cross-root operation the shared cross-root identifier
OperationOne per package operation: kind, package, root
FileOne per file operation: final path, staged path, backup path, action
DirectoryOne per directory the transaction created, so rollback can remove it
Commit payloadFor a cross-root transaction, the state a roll-forward would need

Recording intent and committing are ordinary database writes, and the journal inherits the database's transactional guarantees.

7.5.2 Integrity #

The database is stored under a security descriptor granting write access to the tier of principals permitted to install packages. That descriptor is the journal's integrity protection: a principal outside the tier cannot forge an entry, and one inside it already holds installation authority, so a write from within is not an escalation.

The staging area is under the same descriptor.

7.5.3 Attribution #

Claim link operations do not have an operation row of their own within an install. They are appended to the last staged package operation as a carrier, so the file rows recording a claim link change are attributed to whichever package sorted last. A standalone grant or revoke uses a synthetic operation named for the role, and is attributed correctly.

The consequence is confined to history display; recovery is action-agnostic and unaffected.

7.5.4 Versioning #

Each transaction records the journal schema version it was written under. A peipkg version that can read that schema recovers the transaction directly; one that cannot refuses, with an error naming the schema version rather than a generic failure.

That is what makes upgrading peipkg itself unremarkable (§8.5): the binary running the next recovery may be a different version from the one that started the transaction, and the version stamp is how it knows whether it can.

7.6 Side-Effect Batching

Peios / Peios technical reference / peipkg / Transactions

A transaction may contain several operations that each declare the same side effect. They are deduplicated and invoked once per transaction, after every file-level operation is complete and after the database commit.

Distinct side effects are invoked in an unspecified order. The recognised set is chosen so that order between them does not matter.

7.6.1 Timing #

Side effects are not invoked during extraction, and not once per package. Installing ten packages that all ship shared libraries rebuilds the library cache once, after every package's libraries are in place — not ten times against ten partial states.

7.6.2 What is not scheduled #

An operation that only removes files schedules no side effects, because side-effect declarations are read from the package being installed and a removal has none in flight. The removed package's manifest is in the database and could supply them.

The visible consequence: uninstalling the last package that owned a shared library leaves the library cache naming a file that no longer exists, and removing kernel modules leaves the module dependency cache stale. Both are corrected by the next transaction that does declare the relevant effect.

An upgrade is different: side effects implied by files the upgrade removed are scheduled alongside those the new version declares.

7.7 Visibility

Peios / Peios technical reference / peipkg / Transactions

The database state visible to a query reflects only committed transactions.

Reads are snapshot-isolated: a query beginning at some moment sees a consistent view of committed state as of that moment, regardless of a write transaction committing while it runs. This is a property of the store rather than of peipkg's use of it, and it is why a read-only query needs no lock.

Committed package metadata is not the same as filesystem or journal visibility. A pending journal is evidence of work in progress, and staged files move to their final paths during application. In the reviewed cross-root path, preparation persists recovery data and applies file moves before the per-root package-state commits. Outside processes can therefore observe files from a transaction whose package metadata has not committed.

peipkg does inspect its own uncommitted state — verifying a staged file, computing what remains to apply — which is not a visibility leak.

7.7.1 Where the boundary is softer #

Several things about an in-flight transaction are observable from outside.

Staged files and backups are siblings of their destinations rather than files in a private directory, so they are visible in a directory listing under names carrying the transaction identifier. They are not at the paths anything would look them up by, but they are there.

And within the apply phase, a file is momentarily absent between its original being renamed aside and its replacement being renamed in. Anything opening that exact path in that window sees nothing.

Across roots, commits and post-commit maintenance happen root by root. A snapshot of one database does not prove the state of every participant or which binaries a running process has mapped. See Atomicity and the operator recovery checks. These cross-root observations are pinned to peipkg 8b588ae8, not a runtime test or a guarantee about every shipped revision.

7.8 Crash Recovery

Peios / Peios technical reference / peipkg / Transactions

Before permitting new work, peipkg checks the journal for a pending transaction.

  • No pending transaction — nothing to do.
  • A pending transaction — roll it back. Restore every displaced original from the backup map, remove the directories the transaction created, discard its staged files, and clear it from the journal.

Recovery is itself crash-safe: every action it takes is a rename from the backup map, and every step checks the current state before acting, so re-running after an interruption converges on the same result.

7.8.1 Rollback rather than roll-forward #

For a single-root transaction there is no roll-forward. Because the database commit is the single durability boundary and is itself atomic, a recovered transaction is only ever committed — in which case the database says so and there is nothing to recover — or pending, in which case it rolls back.

Note

Roll-back-only recovery is possible because the database is a transactional store. A design that applies file changes after a separate commit-intent record needs roll-forward to finish what that record promised. Here the database commit and the fact that the transaction is done are the same atomic event, so there is nothing left to finish.

7.8.2 Cross-root: the exception #

A cross-root operation commits one root at a time. Once a root has committed, its transaction is done and cannot be undone by rolling back a sibling.

In peipkg 8b588ae8's coordinated recovery, pending participants roll back if none committed. If a sibling has committed, recovery completes pending participants from persisted completion data instead. Missing or malformed data needed for roll-forward causes refusal. Recovery can itself fail after another participant has been reconciled.

Keep the named-root topology intact and make every participant reachable from the same anchor used for the operation. The CLI's reachable-root scan does not establish that absent or unregistered participants are detected. Its rolled back cross-root transaction message also covers successful roll-forward; inspect per-root history instead of treating that phrase as the recovery direction.

The roll-forward path applies persisted database metadata and attempts backup cleanup. It does not invoke the normal post-commit side-effect runner. The operator recovery checks cover verification and the remaining maintenance work. These statements are pinned to source, not a runtime test or a guarantee about every released image.

7.8.3 When it runs #

Recovery runs at the head of every install, upgrade, and uninstall, before the requested work begins and under the lock. It is not something an operator invokes; peipkg recover exists, but the ordinary path is automatic.

A pending cross-root transaction discovered by a single-root operation is refused rather than recovered, and blocks all further work in that root until peipkg recover is run.

Automatic recovery emits no audit event. The recover command does, so a rollback recovered by the next ordinary install leaves no record in the audit stream while the same rollback performed deliberately does.

8.1 Transaction Rollback

Peios / Peios technical reference / peipkg / Rollback and Recovery

A transaction can be rolled back at any point before the database commit (§7.4 step 3). After that commit the transaction is committed, and what remains is cleanup rather than rollback.

Important

The procedure below retains an earlier single-root description; it is not a newly verified implementation contract or a manual recovery recipe. In the reviewed cross-root path, a pending participant cannot be rolled back in isolation after a sibling commits. Coordinated recovery may need roll-forward, and rollback errors retain a pending journal, rather than unconditionally closing it before restoration. Start with operator recovery for that path and its source/release scope.

8.1.1 When it happens #

  1. Any step of any operation fails — a hash does not verify, disk space runs out, a security descriptor is rejected.
  2. The operator cancels.
  3. The process terminates abnormally before commit, in which case rollback runs on the next invocation (§7.8).

8.1.2 The procedure #

  1. Discard the transaction's staged files.
  2. Leave the pending database changes uncommitted — the commit never ran — and clear the transaction from the journal.
  3. Restore every displaced original by renaming its backup back into place.
  4. Remove the directories the transaction created.
  5. Release the lock.

Side effects are not involved. They run only after the database commit, so a rolled-back transaction never reached one and there is nothing to undo.

8.1.3 Ordering #

File operations are reversed in the opposite order to the one they were applied in, and each step checks the current state before acting, so a rollback interrupted partway and re-run reaches the same result.

8.2 Backups

Peios / Peios technical reference / peipkg / Rollback and Recovery

A backup is made by renaming the displaced original aside within its own directory, never by copying it.

The old file's content is retained in place under a different name, so a backup costs no additional disk space and is produced by a single atomic rename. The journal's backup map records, for each displaced file, the name its original was renamed to.

Restoring a backup on rollback is the inverse rename. Discarding one on commit is a delete.

8.2.1 Naming #

A backup and a staged file are both siblings of the destination, in the destination's own directory, under names carrying a marker and the transaction identifier. Where the destination's basename is long enough that adding the marker would exceed the filesystem's name limit, the basename is truncated.

Two long sibling basenames that differ only past the truncation point therefore produce the same temporary name.

8.2.2 Retention #

Backups are discarded as soon as the transaction commits.

The design permits keeping them beyond commit for a configured window, to support reverting a committed transaction from local state. That is not implemented, which is why peipkg undo works by re-resolving against the archive index rather than by restoring backups (§6.5), and why undo needs a reachable repository.

8.3 Completeness

Peios / Peios technical reference / peipkg / Rollback and Recovery

A successful rollback leaves the system indistinguishable from its state immediately before the transaction began: file contents and existence as before, the package database as before, and the journal carrying no pending transaction.

Security descriptors come back with the files. A restore-by-rename preserves a displaced original's descriptor exactly, because the file was never rewritten. For newly created content the question does not arise, since the file is removed rather than restored.

8.3.1 When rollback itself fails #

A rollback can fail: an I/O error, a filesystem gone read-only, a permission change mid-operation. The intended behaviour is that such a rollback is reported as a failed rollback, the system is treated as indeterminate, and further transactions are prevented until an operator resolves it.

Earlier wording here said rollback errors were discarded at every call site and the journal closed as rolled back regardless. That is not the reviewed cross-root path: in peipkg 8b588ae8, rollback failure is reported and the affected journal remains pending. Follow the cross-root recovery checks with all participants reachable. This source correction does not establish the behaviour of every other rollback path or every installed revision.

If inspection instead establishes the previously described closed-journal failure, its consequences are:

  • the failure is not reported;
  • the transaction leaves the pending state, so the next invocation's recovery finds nothing and does not retry;
  • some originals remain at their backup paths and some new files remain at their final paths;
  • the history shows an authoritative-looking rolled-back record;
  • the database and the filesystem disagree, with nothing to reconcile them.

peipkg verify can identify recorded files whose hashes no longer match what is on disk. Use it alongside transaction state and the operation's diagnostics; a rolled-back history label alone is not proof of restoration.

8.4 Indeterminate State

Peios / Peios technical reference / peipkg / Rollback and Recovery

A failed rollback, a corrupt journal, or an unrecoverable backup mismatch leaves the system in a state peipkg cannot reason about.

8.4.1 The intended handling #

A recovery mode with five properties:

  1. Every write operation — install, upgrade, uninstall — is refused until recovery completes.
  2. Read operations proceed but carry the indeterminate-state warning in their output.
  3. A forensic report is available on demand, identifying the pending transaction's operations, files whose on-disk content does not match the recorded hash, database records inconsistent with the journal, and any orphaned staged files or backups.
  4. An explicit resolution command accepts an operator decision: roll the pending transaction back, or accept the current on-disk state and discard the journal and backups.
  5. Resolution is a deliberate operator action and is never performed automatically.

8.4.2 What exists #

The list above is proposed handling, not a supported recovery interface. Earlier wording here said recovery only rolls back and writes are never refused after failure. Do not apply those blanket statements to the source-verified cross-root path: rollback errors retain pending work, an ordinary single-root operation refuses to recover it in isolation, and coordinated recovery can roll forward after a sibling commits.

That correction does not establish the five proposed facilities or verify every single-root failure path. Inspect the actual journal, diagnostics and installed revision, and use the operator recovery checks rather than assuming a universal recovery direction or write policy.

8.4.3 What an operator can do today #

peipkg verify re-hashes every recorded file against what is on disk and reports the differences. That is the closest available thing to the forensic report, and it is the tool for establishing what a failed operation actually left behind.

peipkg recover explicitly attempts recovery and emits an audit event where the automatic path does not. Coordinated cross-root recovery chooses rollback or roll-forward from participant state; its console message alone does not establish that direction.

Beyond that, reconciling the database with the filesystem is manual: identifying files sitting at backup paths, deciding whether the old or the new content is wanted, and reinstalling the affected packages to restore agreement.

8.5 Upgrading peipkg Itself

Peios / Peios technical reference / peipkg / Rollback and Recovery

Upgrading the package manager is not a special case.

The peipkg binary is one file among a transaction's payload, staged beside itself and renamed into place like any other. The swap is a single atomic rename, so there is no half-written binary to recover from: recovery reconciles a transaction that crashed between its atomic steps, not a file caught mid-write.

8.5.1 The one wrinkle #

After a self-upgrade, the binary running the next recovery may be a different version from the one that started the transaction.

This is handled by versioning the journal format. Each transaction records the schema version it was written under. A peipkg version that can read that schema recovers the transaction directly; one that cannot refuses with an error naming the schema version, leaving the transaction for a version that can.

No immutable copy of the previous binary is required. If recovery needs the prior binary, it is already present as that binary's ordinary backup — until the transaction commits, at which point backups are discarded (§8.2).

Note

A package-manager upgrade that commits successfully but installs a defective binary is not a recovery case: there is no incomplete transaction. It is handled by running the prior binary — retained as the transaction's backup, if the transaction has not yet committed — to perform an ordinary downgrade. Once the transaction has committed and the backups are gone, the route is a downgrade using whatever peipkg is now installed, or an externally supplied package file.

8.6 Reverting a Version

Peios / Peios technical reference / peipkg / Rollback and Recovery

Reverting an installed package to an earlier version is an ordinary downgrade, and runs through the ordinary transaction machinery with the ordinary guarantees.

8.6.1 The procedure #

  1. Query the archive index for the available versions of the package.
  2. Select the desired one.
  3. Run an upgrade with that version as the new version, which requires explicit downgrade authorisation (§6.5).

8.6.2 Constraints #

The target has to be available from a configured repository's archive index, or already cached locally. A version pruned from the archive cannot be reached without an externally supplied package file.

Reverting may require adjusting dependents whose constraints the older version does not satisfy. The resolver determines that, and the plan may include further downgrades or removals.

8.6.3 Scope #

Reverting a package's version reverts that package's payload and nothing else. Registry state, configuration materialised by reconcillers, runtime data under /var/, and user data are unaffected.

Note

Comprehensive system rollback, including state under registry control, is a higher-level concern handled by recovery snapshots and other mechanisms outside the package manager. Package-level version reversion covers the common case of "this update broke something; put it back".

9.1 The Model

Peios / Peios technical reference / peipkg / Roles and Claims

A role is a virtual name several installed packages may contend to own on the filesystem, with at most one holding it. The holder's file answers the contended path through a symlink peipkg owns.

Two registry daemons can be installed at once; only one of them is /usr/sbin/registryd.

The declarations are specified in PSPU §5.23. What follows is what peipkg does with them.

9.1.1 The pieces #

A role has one or more slots, each materialising one filesystem name. A slot's claim paths are where the links appear; a target is the holder's file each link points at.

The claim-path set for a slot is the union of every installed consumer's declared path for it, plus the holder's own provider-declared default path if it has one. The materialised links are the cross-product of that set with the holder's targets.

9.1.2 Links are relative #

A claim link's body is a relative path, computed from the link's location to the target: a link at /usr/sbin/registryd pointing at /usr/sbin/loregd is written as loregd.

Only the database keeps the absolute logical target. Anything reading the link itself — an auditor, an unrelated tool — sees the relative form.

The reason is relocatability. A root assembled by the composer, or installed under an alternate root, or packed into an initramfs archive, is moved around before it is ever booted. An absolute link body would point outside it.

9.1.3 Links belong to peipkg #

A claim link is owned by the package manager, not by any package. It never appears in a package's payload and is never recorded as a package-owned path.

That is what lets two eligible providers coexist: neither ships the contended path, so the one-package-per-path rule is never engaged by the providers themselves.

9.2 Eligibility

Peios / Peios technical reference / peipkg / Roles and Claims

A package is an eligible provider of a role when it has a provides entry naming the role whose claims field declares a target for at least one slot. Only an eligible provider can hold a role.

A package that depends on a role and declares a claim path for it, without providing the role, is a consumer only: it contributes paths and can never hold.

9.2.1 What peipkg checks #

The declaration shape is validated on both sides. A consumer-side slot descriptor carries a path and no target; a provider-side one carries a target and optionally a path. A claims field on a conflicts entry is rejected outright. Slot names are validated against the package-name grammar.

Claim paths and targets are held to the payload path-syntax and safety rules of §5.13 — absolute, valid UTF-8 in Normalization Form C, no NUL, control or backslash bytes, no empty, . or .. component, and within the component, depth and total length limits. The manifest decoder applies the same single copy of those rules that the archive reader applies to a payload path, so the two cannot drift.

A claim path is also confined to the locations §5.23 admits: under one of the permitted install destinations of §5.14, under /run/, or the root-level name /init. A manifest declaring a claim path anywhere else — /etc/passwd, /opt/tool, /run itself, /init/x — is rejected when it is decoded, before anything is planned. A provider's target is confined to the §5.14 destinations alone, because it names a payload path: /run/ and /init are claim-only locations that no package can ship to.

9.2.2 Targets are checked against the package's own payload #

A provider's target must name a path the declaring package itself installs, and peipkg checks that at install time against the payload it actually received. The producer-side library offers the same check and pekit runs it, but a producer-side check is a lint: it says nothing about a package built anywhere else, which the format explicitly contemplates.

The check also constrains targets by destination for free. A target that must be a payload path inherits the payload path rules, because those are enforced on the same entries.

9.2.3 Existing content at a claim path #

/lcl/policy is unreachable as a claim path or target whatever else permits it (§5.14), and the destination set keeps a claim link inside the managed tree. A newly materialised link also follows the unowned-file rule (§5.7): an identical relative symlink is adopted without replacing its inode; differing content is refused unless overwrite was explicitly authorised. An authorised replacement retains the displaced content at the transaction's backup path and reports that path in a warning.

This applies both during package installation and during a standalone claim grant. Every command other than install has no overwrite flag, so conflicting unowned content must be moved aside deliberately before retrying. The shared refusal message recommends this recovery for every command; install can also use its --overwrite-unowned option. Repointing a recorded claim link remains an atomic replacement. A path owned by a package that will remain installed is refused (§9.6).

9.3 Auto-Claim

Peios / Peios technical reference / peipkg / Roles and Claims

Installing a package that is an eligible provider of one or more roles claims, by default, every one of those roles that is currently unheld. The new provider becomes the holder, and the role's claim paths are materialised against its targets.

Auto-claim applies only to unheld roles. Installing a provider of a role another package already holds does not change the holder: the new provider is installed and eligible, and the incumbent keeps the role unless the operator directs otherwise.

Note

The two halves are deliberate. Auto-claiming an unheld role means a freshly installed sole provider just works — install the registry daemon and its binary appears, with no second command. Leaving held roles alone means installing an alternative provider never silently steals a live system name out from under the incumbent.

9.3.1 Two providers in one transaction #

When a transaction installs two eligible providers of the same unheld role, the role goes to the one whose package name sorts lexicographically first.

The rule exists so that the outcome is a consequence of the inputs rather than of the order the resolver happened to place them in, which would make an install non-deterministic.

9.4 Install Flags

Peios / Peios technical reference / peipkg / Roles and Claims

Three flags modify what an install claims. Let the provided roles of a package be the roles it is an eligible provider of.

FlagEffect
--no-claimClaim nothing, including unheld roles
--claim <roles>Claim each named role, even if another package holds it
--claim-allClaim every provided role, including held ones

The claim set applied is the union of two sets:

  • the auto set — the provided roles currently unheld — which is empty under --no-claim; and
  • the force set — the roles named by --claim, or every provided role under --claim-all.

Roles in the force set are claimed even when held. Roles in the auto set are claimed only because they are unheld.

Note

The flags compose. --no-claim --claim registryd empties the auto set and forces exactly one role: it claims that role and nothing else, not even a second unheld role the package provides. This is the idiom for "claim only this".

9.4.1 Contradictions #

Two combinations are rejected as self-contradictory: --claim-all with --claim, and --claim-all with --no-claim.

--no-claim with --claim is not contradictory — it is the idiom above.

9.4.2 Naming a role the package does not provide #

A role named by --claim that nothing in the transaction provides is rejected: a package cannot hold a role it is not an eligible provider of.

The check is against every package in flight, not only the one the operator named. So peipkg install foo --claim bar succeeds when a package pulled in as a dependency of foo provides bar, and claims it for that package.

9.5 The claim Command

Peios / Peios technical reference / peipkg / Roles and Claims

peipkg claim inspects and changes holders independently of install.

FormEffect
peipkg claim <role>Report the role's holder, its materialised claim paths, and the installed eligible providers
peipkg claim <role> grant <package>Make the named package the holder, repointing every claim link
peipkg claim <role> revokeRevoke the current grant, removing the links and leaving the role unheld

The bare form lists every installed eligible provider, including the current holder.

A grant to a package that is not installed, and a grant to a package that is installed but not an eligible provider, each fail with their own error.

A revoke on a role that is not held is an error rather than a no-op.

A grant or revoke prompts for confirmation, satisfied by --yes, and executes within a transaction that rolls back on failure like any other.

9.5.1 Addressed by role #

A role is addressed by name, never by path. Because a role has one holder across all of its slots and paths, granting it moves every one of its claim paths together.

Note

Addressing by name rather than by path keeps a multi-path, multi-slot role coherent: one command moves the whole role, however many files it materialises. A path-addressed form would invite a role being half-moved.

9.6 Materialisation

Peios / Peios technical reference / peipkg / Roles and Claims

Materialisation is reconciliation: peipkg recomputes what the role's links should be from current state, and applies the difference.

9.6.1 When it runs #

Every transaction recomputes the desired link set over all post-transaction manifests and all holders — not only for roles whose holder it changed.

That is what makes retroactive materialisation work. Installing a package that merely declares a consumer-side path for a role another package already holds creates the link against the existing holder, and does not re-open the question of who holds it. Removing such a package removes the links only it declared, leaving paths other packages still declare in place.

Reconciliation is idempotent: reconciling against unchanged state produces no filesystem change.

9.6.2 Held but unmaterialised #

Holder state lives in its own table, keyed by role, and is never inferred from whether a link exists.

A role whose computed claim-path set is empty materialises no links and remains held. Installing a package that declares a path for it later materialises the link retroactively, against the holder already on record.

Note

A sole provider installed before any consumer is held but unmaterialised: it holds the role, but until some package declares a dependency-side path — or the provider itself declares a default — there is no path to point anywhere. Tracking holder state independently of link existence is what lets the later install create the link without re-deciding the holder.

9.6.3 Collisions #

Before materialising a link, peipkg checks that no installed package owns the claim path — judged against the state the transaction will produce, not the state it started from.

The distinction is load-bearing in both directions. An in-flight package's ownership rows are not written until commit, so reading the table directly would let a transaction that installs both a package owning a path and a provider claiming it see no owner, queue a payload operation and a claim operation for one destination, and commit both. And it would refuse the legal case: uninstalling the owner in the same transaction that materialises the claim.

That freed-path case works. The removal's own file operation is dropped and the claim's takes over the displacement, because two operations on one path would journal two rows for one destination and contend for one backup name.

A package owning a directory at the claim path is a hard collision. A link cannot occupy a directory path, and treating it as permissible meant renaming the package's directory aside to make room — relocating it and its contents, after which the non-empty backup could not be removed at commit and was left behind.

The reverse direction is guarded too. A package payload whose destination is a live claim path is refused, naming the role. Allowing it would have been unrepairable: reconciliation compares its record of the link set against its own desired set, finds them equal, and never looks at disk, so a link displaced by a payload was gone permanently with the database still asserting it. The link belongs to the manager, placed because a role names it; a package that wants the path is in conflict with the role rather than entitled to take it.

9.6.4 Repointing #

A holder swap repoints every one of the role's links, within a single transaction, and each repoint is atomic. The new link replaces the old in one step, and the displaced link is then moved to the backup path a rollback restores from.

Note

Atomic repoint matters because a claim path is typically on the critical path of a running system — a contended daemon binary may be executed at any moment. Two renames leave the path absent for a full rename round-trip, on every link of the role, which is long enough for a supervisor to get ENOENT from an exec.

The payload path deliberately keeps backup-by-rename instead. There the displacement has to happen before the new content lands, because the displaced original is what a rollback restores.

9.7 Withdrawal

Peios / Peios technical reference / peipkg / Roles and Claims

When the package holding a role is uninstalled, the role's claim is withdrawn: peipkg removes the role's links within the uninstall transaction and the role becomes unheld.

peipkg does not automatically promote another eligible provider.

If other eligible providers remain installed, the withdrawal is surfaced to the operator, naming them and the command to assign a new holder:

Claim 'registryd' withdrawn — packages 'org.example.altregd', 'org.example.thirdregd'
also provide it. Run 'peipkg claim registryd grant org.example.altregd'
to assign a new holder.

Note

Withdrawal leaves the role unheld rather than auto-promoting because the replacement is the operator's choice: the remaining providers are alternatives that were deliberately not chosen while the incumbent held the role. Auto-promoting one would silently repoint a contended system name at a provider nobody selected. Surfacing the alternatives with a ready-to-run command makes reassignment one step without making it implicit.

9.7.1 Revocation is not withdrawal #

Revoking a role is the operator taking a grant away. Withdrawal is the same end state reached automatically when the holder is removed — a package withdrawing its own claim rather than an operator revoking a grant.

A revoke does not surface the remaining providers the way a withdrawal does, even though the resulting state is identical.

9.7.2 Whether withdrawal is safe #

It depends on the role's consumers. If every installed consumer reaches the role through an optional dependency, withdrawal breaks no required dependency and is informational.

If a required dependency is left with no holder for a name it hard-codes, the operator is removing something the running system needs. That is the scenario a system-critical guard would catch, and peipkg has no such guard (§4.6).

9.7.3 Ordering within an uninstall #

Because claim operations ride the last staged package operation, an uninstall of the holder deletes the target file before it deletes the link. There is a transient window within the transaction in which the link dangles — invisible to anything that only sees committed state, but visible to a concurrent reader.

10.1 Named Roots

Peios / Peios technical reference / peipkg / Installation Roots

An installation root is a self-contained filesystem tree with its own package database. The default root is the system root — the anchor. A system may define others; the motivating case is an initramfs image, built and maintained alongside the main system through the same package graph.

A package names a root, never a filesystem location. Where a root lives is the installing system's business, which is what makes a tree relocatable and what stops a package dictating layout.

10.1.1 The grammar #

A root reference is one or more segments joined by ., each matching [a-z0-9][a-z0-9_-]*. Any reference containing / is rejected as a reference.

The grammar is implemented three times — in the manifest decoder, in the command-line parser, and in the producer toolchain — and all three agree, all three reject a /.

10.1.2 Nesting is structural #

There is no parent field anywhere. A root's registry lives in that root's own database, so nesting falls out of where the registration is recorded: initramfs.subroot is resolved by reading the anchor's database for initramfs, then reading that root's database for subroot.

Registered paths are stored relative to the owning root, which is the other half of relocatability.

10.2 Registration and Resolution

Peios / Peios technical reference / peipkg / Installation Roots

10.2.1 The registry #

A root's named children are rows in that root's own package database: name, path relative to the owning root, and creation time.

The registry is runtime fact rather than configuration. It records where a root actually is, so it lives with the rest of what the system knows about itself, not in the operator's configuration tree.

peipkg root add, remove, list, and show manage it. The composer registers roots declaratively from its manifest.

10.2.2 Resolving a reference #

The --root option accepts either form, and the discriminator is explicit:

  • A reference containing / is a literal filesystem path, used unchanged.
  • A reference without / is a name. It is split on . and walked segment by segment: open the current root's database, look up the segment, join its relative path, and recurse into that root's own database for the next segment.

An unregistered segment is a hard error. peipkg never creates a root implicitly.

A visited set of resolved absolute paths rejects cycles, so a registry that points a root at one of its own ancestors fails rather than looping.

A manifest may never carry the path form. The discriminator exists only at the command line, where an operator legitimately wants to install into a directory that is not a registered root — a mounted target, a scratch tree.

10.3 Cross-Root Dependencies

Peios / Peios technical reference / peipkg / Installation Roots

A dependency is satisfied within a root. By default that is the depending package's root, so a package's closure flows into the root the package occupies. A dependency's root field names a different one.

{ "name": "peiosutils", "root": "initramfs" }

declares that the dependency is required in the initramfs root, wherever the depending package lives.

10.3.1 Routing #

Every placement decision passes through one point: given a dependency and the depending package's root, choose the target root. An empty root field, or single-root mode, keeps the dependency where the depender is; otherwise the name is looked up in the live registry.

A root naming something not registered is a resolution failure — unsatisfiable, naming the root — rather than a silent fallback.

Cross-root edges are honoured in plan ordering and in the reverse-dependency check that decides what a removal breaks.

10.3.2 Which verbs route #

install resolves cross-root. In peipkg 8b588ae8, upgrade also resolves one combined plan across the current root and its reachable named roots by default. --no-recurse confines upgrade resolution and execution to the current root. The resolver setup honours the root named by a cross-root dependency during that default upgrade. See Named roots for the operator flow, transaction boundaries and source/release qualification.

The remaining verbs, downgrade, uninstall, and undo, were described here as single-root resolution, where a dependency's root field is inert. The pinned upgrade correction above does not verify those other paths; do not infer that every verb has the same cross-root routing.

10.3.3 Satisfier identity #

A satisfier is identified by the pair (name, root). The same package name installed in two roots is two independent installations, possibly at different versions, and a dependency is satisfied only by an installation in the root it names.

Note

Cross-root dependencies are what let a root be composed through the dependency graph. An initramfs package depends on ordinary packages — a shell, core utilities — and has them placed into the initramfs root, either implicitly by living there or explicitly through root. The depended-on package declares no root affinity of its own; where it lands is the depender's and the operator's choice. This generalises the fixed build-host/target split other systems draw to an open set of named roots.

10.3.4 Top-level placement #

Where an operator names a package directly with no explicit --root, the package's default_root decides where it lands. peipkg applies it once, before building the resolver's requests, and only when the operator did not pass --root — a defaulted root does not count as explicit.

If the named packages declare no default, the current root is used. If they declare exactly one distinct default, the whole install is re-rooted there. If they declare two or more different defaults, peipkg stops and tells the operator to split the command or pass --root, rather than picking one.

A dependency's placement is never governed by its own default_root; only by the depending package's root and the dependency's root field.

10.3.5 Cross-root garbage collection #

Removing the last thing in a root that required a cross-root dependency does not remove that dependency from the other root. Cross-root autoremoval is not implemented; the dependency stays until something removes it explicitly.

10.4 Composing a Root

Peios / Peios technical reference / peipkg / Installation Roots

peipkg-compose builds a populated root from nothing: offline, deterministically, without touching the host filesystem and without executing any package's code. It is the counterpart to peipkg, which mutates a live system.

It is not an image builder. An outer tool calls compose, then chroots to pack the initramfs, squashes the root, and builds the boot image.

10.4.1 The manifest #

A TOML document with a schema version, declaring:

KeyMeaning
archThe primary architecture, which becomes the composed database's recorded value
source_dateThe composition timestamp recorded in the package database and generated metadata
local_packagesGlobs of package files on the build host that join the candidate set — the bootstrap path
[[repository]]Name, base URL, priority, signature policy, trust anchors, transport allowance, minimum index version
[[root]]A name and a path, declaring a named root nested inside the output
[[package]]A name, an optional version constraint, an optional repository pin, an optional root

Unknown keys anywhere are rejected. A package pinning an undeclared repository, or placed in an undeclared root, is an error. Package identity is (root, name), so the same package may be requested in two roots.

10.4.2 The lock #

Resolution writes a lock: the pinned closure, with each package's source, absolute URL, hash, root, and the compressed and installed sizes its repository index advertised. It carries a digest of the manifest, so that building against a stale lock is caught.

The lock also records, per repository the closure draws from, that repository's signature policy and the trust set resolution established for it — each key's fingerprint, public bytes, status, and any expiry. Trust is established once, at resolve time; recording it is what lets the build phase verify signatures without repeating the ceremony. A lock naming a repository package whose source it records no trust state for is rejected as malformed.

The digest covers the packages, their constraints, their repository pins, the [[root]] declarations and each package's root placement — everything that influences resolution. Moving a package between roots, or changing where a root lives, changes the digest, so a build refuses the stale lock rather than silently reproducing the previous placement.

The lock's root key is a path relative to the output, while the manifest's root key of the same name is a name. A lock is therefore bound to one root layout.

10.4.3 The three phases #

Resolve performs the full trust ceremony for each declared repository in a throwaway database that never reaches the output, fetches the active index — and the archive index when a constraint might need historical versions — and reads any local package files into synthetic candidates. Manifest pins filter the candidate set. Resolution then runs against an empty installed set.

Elevated actions the plan implies are printed as warnings and the build proceeds, because compose runs unattended with nobody to authorise them.

Fetch downloads every package within the bound the lock's size_compressed sets, checks its bytes against the hash the lock recorded, verifies its inline signature against the trust set the lock carries for its source, validates its format, and cross-checks the archive's own manifest against the lock entry. Every package is verified before any is extracted.

Signature verification applies the source's recorded policy: under required an unsigned package is refused, and under either policy a signature made by a key the repository has since marked revoked, or by a transitioning key past its expiry, is refused as well. Key status is evaluated against the clock at build time, not at lock time, so a key revoked after the lock was written stops verifying — a build from an unchanged lock can begin to fail, and that is the intent.

Decompression is bounded by the size_installed the lock carries from the index, and the archive's manifest must declare the same figure. The manifest is inside the stream being bounded, so it cannot supply the bound.

Assemble buckets packages by root and, per root, validates the payload layout against the fetched bytes, resolves claims, seeds the database, extracts payloads, and materialises claim links. The whole tree is built under a temporary name and renamed into place on success.

Payload files and symlinks retain the archive's canonical modification time, which format verification requires to equal the package's build.timestamp. Declared directories are timestamped after extraction, claim links, and generated metadata are complete. A directory shared by several packages uses the newest owning package's timestamp, independent of extraction order. Symlink timestamp updates do not follow their targets. A filesystem that cannot represent the required timestamp causes composition to fail rather than silently changing it.

This keeps an unchanged dependency closure stable across fresh build roots. Changing package identities must still invalidate reused build outputs: a newly selected package can contain different bytes with an older canonical timestamp.

10.4.4 What it seeds #

Into each root's database, in one transaction: the recorded architecture, a package row per package carrying the verbatim manifest, an owned-file row per payload entry, claim holder and link rows, and the named-root registrations.

Owned-file rows are written before any file is extracted, so a cross-package path collision aborts the build before anything lands.

Journal rows are deliberately left empty: a composed root has no transaction history because nothing was ever applied to it incrementally.

10.4.5 What it writes that no package owns #

Two things: a repository configuration file for each declared repository, and a license inventory naming every composed package's license and provenance.

Neither appears in the owned-file table, so neither is upgradable or verifiable by peipkg afterwards. Both land under paths a package could not install to.

10.4.6 What it deliberately omits #

No side effects — a composed root's library cache, module dependency cache, and man index are never built. No security descriptor materialisation. No audit events. No repository trust state or index cache in the output, so the composed system performs its own trust ceremony on first use. The trust state the lock carries is an input to the build, not something the build writes into the root.

10.4.7 Layout enforcement #

The permitted-destination check runs at compose time, against the fetched bytes rather than the producer's word for them, and the two-key rule applies exactly as it does at install: a package declaring itself a special system package composes only when the composition also grants the bypass, and a declaration without the grant produces an explanatory refusal.

10.4.8 Root-level views #

The composer synthesises no root-level runtime view. It creates payload entries, claim links, the repository configuration, and the license inventory, and nothing else. Where a boot root needs the psABI-fixed interpreter path to reach the loader before any view is mounted, that mapping is ordinary package payload from the base-filesystem package — shipped for each independent boot root, including the initramfs — rather than something the composer invents.

10.4.9 Roots with nothing in them #

A declared root that no package is placed in is created anyway, with its own database holding nothing. A declared empty root is a reasonable thing to want — somewhere to install into later — and the alternative, registering a root whose directory does not exist, made --root <name> on the composed system fail when peipkg tried to open its database.

10.5 Compose commands and manifest

Peios / Peios technical reference / peipkg / Installation Roots

For the operator workflow, start with Composing a root. This page keeps the full command and build-artifact contract alongside the implementation description.

peipkg updates an existing root through package transactions, with documented recovery and visibility limits. peipkg-compose is its counterpart on the assembly side. Instead of changing a system that already exists, it builds a fresh, fully package-owned root directory from nothing — offline, deterministically, and verifiably, with no live system involved anywhere in the process.

peipkg-compose is a separate binary, not a peipkg subcommand. You run peipkg-compose, and its two verbs (lock and build) are the whole of its command surface.

Given a declarative TOML manifest that names a package set and the repositories to draw it from, compose produces a populated root directory: package payloads laid out at their installed paths, a seeded peipkg state database at var/lib/peipkg/db.sqlite, and each repository written out as lcl/conf/peipkg/<name>.repo. The result is a legitimate peipkg-managed system — once booted it can manage itself. The operator repository interface requires an explicit peipkg repo add <name> trust ceremony for supplied configuration before use; do not assume a first refresh grants trust.

It is meant for image builders — an outer image-assembly tool, or a person standing one up by hand. It builds the package tree without modifying the host installation, and can run on a non-Peios host or a host of a different architecture. Its lock output and an optional --record-xattrs output are separate artifacts you also choose.

10.5.1 The mental model #

Compose runs in two stages, and the two verbs map onto them.

Stage 1 — resolve. Read the manifest, resolve the requested package set against the declared repositories into a complete transitive closure, verify each repository's trust chain, and pin the result. This stage needs repository metadata; its output is the lock file.

Stage 2 — assemble. Fetch the pinned .peipkg payloads, verify them against the lock's hashes, and lay out the root directory. This stage needs only package bytes, not metadata, and can run fully air-gapped once a lock exists.

lock does Stage 1 alone. build does Stage 2, running Stage 1 first when it needs to. Everything build-stamped in the output is derived from the manifest's source_date, so given the same manifest, lock, and packages, the output is reproducible.

10.5.2 peipkg-compose lock #

peipkg-compose lock <manifest> [-o <lock>]

Resolve the manifest against its repositories, verify the trust chain, and write the lock file. lock builds nothing — it produces only the lock.

The <manifest> positional is required. Flag order is not significant: the manifest may appear before or after -o.

OptionEffect
-o <lock>Output lock path. Defaults to the manifest path with a trailing .toml replaced by .lock.toml — so image.toml locks to image.lock.toml. Only the single-dash -o form exists here; there is no --out for lock.

10.5.3 peipkg-compose build #

peipkg-compose build <manifest> --out <dir> [--locked | --update]
                     [--dangerously-bypass-path-restrictions]
                     [--record-xattrs <file>] [--no-dependencies]

Assemble the root directory. The <manifest> positional is required; flag order is not significant.

--out <dir> is required — omitting it is a usage error. The directory named by --out must not already exist: compose creates it, and refuses to build into or over an existing tree.

OptionEffect
--out <dir>The output root directory. Required. Must not already exist.
--lockedRequire an existing lock and do not resolve. Fails if no lock is present. Fetches only package bytes, taking integrity from the lock's hashes — the air-gap-friendly path.
--updateRe-resolve from scratch, overwrite the lock, then build.
--dangerously-bypass-path-restrictionsPermit packages that declare special_system_package to compose payloads outside the payload layout rules. An image built from a package set including the base filesystem needs this; peiso passes it through from bypass_path_restrictions in the image's build spec, so the grant stays a visible decision of the image rather than something the composer assumes.
--record-xattrs <file>Record each implied security.peios.sig or security.peios.sd attribute in deterministic JSONL instead of setting it on the output tree. Use this when an unprivileged builder will carry the attributes into an image, or when composing a disposable build root. Each line has path, name, and a base64-encoded value; the file must not already exist.
--no-dependenciesInstall exactly the packages the manifest names and ignore their dependencies. The result is not a self-sufficient root: it is for a builder that lifts only those packages' own files out of the composition and meets their run-time needs elsewhere, and so can draw on a repository that does not yet hold their dependencies. Signature and hash verification are unchanged. The lock records no_dependencies = true, and a build refuses a lock resolved in the other mode.

--locked and --update are mutually exclusive.

With neither flag, build follows Cargo's build / Cargo.lock behaviour:

  • if the sibling lock is missing, it resolves, writes the lock, and builds;
  • if the sibling lock is present, it verifies the lock still matches the manifest and then builds.

Use --locked for reproducible or air-gapped rebuilds where no metadata resolution should happen at all, and --update when you want to pick up newly-available versions and refresh the lock in the same run.

10.5.4 The manifest #

The manifest is a TOML file describing the package set to assemble and the repositories to draw it from. Any filename works — it is passed positionally — and its sibling lock is <stem>.lock.toml. The manifest and the lock are tool-local build artifacts; they are not an on-wire Peios format.

The parser is strict: an unknown key anywhere in the manifest is a hard error, not a warning. A mistyped field name fails the build rather than being silently ignored.

schema = 1
arch = "x86_64"
source_date = "2026-07-01T00:00:00Z"

local_packages = ["./bootstrap/*.peipkg"]

[[repository]]
name = "core"
base_url = "https://repo.example.org/core"
priority = 10
signature_policy = "required"
trust_anchors = ["a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"]

[[root]]
name = "initramfs"
path = "boot/initramfs"

[[package]]
name = "base-system"

[[package]]
name = "nginx"
version = ">=1.27, <1.28"
repository = "core"

[[package]]
name = "live-boot"
root = "initramfs"

10.5.4.1 Top-level fields #

FieldTypeRequiredMeaning
schemaintyesManifest schema version. Must be 1.
archstringyesTarget architecture; becomes the image's primary architecture.
source_datestringyesAn RFC 3339 timestamp. Fixes every build-stamped time in the output, for reproducibility — the manifest's SOURCE_DATE_EPOCH.
local_packagesarray of stringsnoPaths or globs of local .peipkg files. A bootstrap path for packages that live in no repository.
[[repository]]array of tablesnoPackage sources. Also written into the image as .repo files.
[[root]]array of tablesnoNamed roots, for a multi-root image.
[[package]]array of tablesyesThe packages to install. Must be present and non-empty — an empty package set is an error.

10.5.4.2 [[repository]] #

A repository table has the same shape as a .repo file. These entries drive metadata fetch and trust verification during the build, and are written verbatim to lcl/conf/peipkg/<name>.repo, so the booted system inherits the same repositories and the same trust anchors.

FieldTypeRequiredMeaning
namestringyesRepository name; also the .repo filename stem.
base_urlstringyesWhere the repository's index and packages are served from.
priorityintnoSelection priority. Default 50.
signature_policystringnorequired (default) or optional.
trust_anchorsarray of stringsnoHex key fingerprints trusted to sign this repository's index.
allow_insecure_transportboolnoPermit non-TLS transport. Default false.
min_index_versionintnoReject an index older than this version.

10.5.4.3 [[package]] #

Each package table names one package to install. Package identity is (root, name) — the same package may be requested in more than one root, but a duplicate (root, name) pair is an error.

FieldTypeRequiredMeaning
namestringyesThe package to install.
versionstringnoA version constraint — a range like ">=1.27, <1.28" or an exact pin like "9.9.1". Omitted, or "*", means no version constraint; the resolver applies its source, architecture and version preferences. See the selection rules and documentation discrepancy.
repositorystringnoPin the source repository. Must name a declared [[repository]].
rootstringnoTarget a declared [[root]] — like peipkg --root TARGET install. Empty means the package's own default_root if it has one, otherwise the anchor root.

There is no default_root key in the manifest. default_root is a property each package carries from its repository index, and compose honours it automatically whenever a package's root is unset — just as the live consumer does.

10.5.4.4 [[root]] #

Each root table registers a named root in the built image.

FieldTypeRequiredMeaning
namestringyesA single named-root segment matching [a-z0-9][a-z0-9_-]* — no dots.
pathstringyesLocation relative to the output root. May not be absolute, may not escape with .., and may not be ..

10.5.5 Documentation differences to check #

This command/manifest interface and the adjacent implementation description are not fully reconciled. The composition reference describes inline signature verification in addition to locked hashes; the hash-only stage summary here is not a reason to bypass signature checks. Its omissions section says there is no security-descriptor materialisation, while this interface documents application or recording with --record-xattrs. Do not assume an uninspected output has the security metadata you require.

The existing interface below retains those documented options and their contracts. Where that difference affects whether an image is safe to deploy, verify the output and the behaviour of the tool version used to build it.

10.5.6 The lock file #

The lock is generated by lock, or written implicitly by a default or --update build. Its header reads generated by peipkg-compose — do not hand-edit; it is never edited by hand.

For the full transitive package closure, the lock pins each package's exact version, architecture, source repository (or local for a local .peipkg), the fetch URL, and the content SHA-256 hash of the .peipkg. It records each package's target root when that is not the anchor, along with the manifest's arch and source_date and a digest of the manifest.

A build refuses a lock that no longer matches its manifest. A mismatched arch, a mismatched source_date, or a changed manifest digest all reject the lock. On the default path the error tells you to re-run with --update to refresh it.

The two explicit flags pin down the two ends of this behaviour:

  • build --locked requires the lock to already exist and does no resolution at all — the reproducible, air-gapped rebuild.
  • build --update re-resolves from scratch and rewrites the lock.

An unattended compose run cannot authorise the elevated actions that the live consumer would stop and prompt for. Rather than fail the build, compose surfaces those as stderr warnings. See Elevated authorisation for what those actions are.

10.5.7 Named roots and claims in a composed image #

A composed image can be multi-root, and both mechanisms settle more simply here than on a live system — the detail lives on their own pages; this is only how compose relates to them.

Named roots. Each [[root]] becomes a named-root registry entry in the built image's database, so the booted system resolves --root <name> and cascades upgrades into those roots. Every root is a subdirectory nested under the single --out tree, and the whole multi-root image is assembled as one tree — compose needs none of the consumer's cross-root transaction machinery. The repositories and the named-root registry live at the anchor root.

Claims. A fresh build is the simplest case of claim reconciliation: there are no incumbents and exactly one known, closed package set, so every provided claim is auto-claimed. When two packages provide the same claim, the holder is chosen deterministically — the lexicographically smallest package name, matching peipkg install. Claim symlinks are written with targets relative to the link's own directory, so the root stays self-contained and relocatable.

10.5.8 What ends up in the root #

A successful build leaves a directory that is a valid peipkg-managed system:

  • every package's payload laid out at its installed paths;
  • a seeded peipkg state database at var/lib/peipkg/db.sqlite, recording the installed set, the named-root registry, and the settled claims;
  • each declared repository written to lcl/conf/peipkg/<name>.repo, so the booted system inherits its repositories and their trust anchors.

The build is atomic as a whole tree. Because --out must not already exist, compose assembles into a sibling staging directory on the same filesystem and, on success, renames it into place atomically. A failed or interrupted run leaves the staging directory behind for inspection rather than a half-populated --out — there is no partial output to clean up or mistake for a finished one.

10.5.9 What compose does not do #

Compose's contract stops at producing a valid peipkg root. It is deliberately narrow.

  • Not a full image builder. It produces only a directory tree — no bootloader, kernel, initramfs contents, registry seed, or peinit wiring. Those belong to whatever assembles the image around it.
  • Not a live-system tool. It never touches the host /. There is no three-phase transaction, no commit boundary, no rollback journal, and no crash-recovery — the output is disposable, and its only atomicity is the single whole-tree rename above.
  • Not the producer side. It does not build or sign .peipkg files and it does not serve repositories — building and signing is pekit's job, and publishing is peipkg-repo's. Compose consumes ordinary peipkg packages and repositories unchanged, exactly as PSPU §5 defines them.
  • No side effects. depmod and man-db are not run, and no audit events are emitted. The composer does not build these caches; see Invocation for how package transactions schedule them.
  • Security descriptors are applied, where a package declares one. Their security.* namespace normally requires privileged host access. An unprivileged image builder can pass --record-xattrs to receive the implied attributes as JSONL and write them into the image directly instead; the composed directory then carries none of them. Unlike peipkg install, compose applies or records overrides unconditionally: composing a root from nothing is the operator's own act, and there is no running system whose access control could be changed behind their back.

No environment variables are read, and there is no --version flag.

10.5.10 Exit status #

CodeMeaning
0Success — or the help output (-h, --help, help).
1A compose operation failed — resolution, trust verification, a fetch, a hash mismatch, a stale lock a --locked build could not use, or a file operation.
2A usage error — no arguments, an unknown verb, a missing manifest, a missing --out, extra arguments, --locked with --update, or a bad flag.

11.1 The Recognised Set

Peios / Peios technical reference / peipkg / Side Effects

A package cannot ship code that runs at install time. It can declare that one of two standard maintenance operations is required, and peipkg invokes it.

IdentifierRebuilds
depmodThe kernel module dependency cache — modules.dep and its companions under /usr/lib/modules/<release>/
man-dbThe man page index, /var/cache/man/index.db or its equivalent, which apropos and whatis read

The set is closed. A manifest declaring anything else — ldconfig included: Peios has one shared-library directory and no loader cache (§5.24) — is rejected, and a duplicate within the array is rejected.

11.1.1 When each is required #

A package containing kernel modules declares depmod; one containing none does not. A package containing man pages is expected to declare man-db — a recommendation rather than a requirement, because man page lookup degrades to a filesystem scan without it.

peipkg validates the declared values against the enumeration. The consumer does not validate them against the payload; the producer's packer does, refusing a payload whose kernel modules and depmod declaration disagree in either direction. A package built elsewhere that declares depmod with no modules behind it runs nothing at install time and is reported as a warning (§11.2).

11.1.2 What side effects are not #

  • Not a general install-script mechanism. The closed enumeration is precisely what prevents arbitrary code execution at install time.
  • Not a way to register a service. Service integration belongs to the higher-level artifacts that compose packages.
  • Not a way to seed registry state.
  • Not a way to apply security descriptors, which belong to file creation.

A package whose required behaviour cannot be expressed through the manifest is incomplete and cannot be installed through the package format alone. That behaviour is supplied by the artifact that composes the package.

11.2 Invocation

Peios / Peios technical reference / peipkg / Side Effects

Each side effect maps to one fixed command, shaped by the installation root the transaction acted on.

IdentifierHost root (/)Alternate root
depmod/libexec/depmod -a <release>, once per affected kernel release/libexec/depmod -b <root> -m /usr/lib/modules -a <release>, once per affected release
man-db/bin/mandb -qNot invoked; the operation report says so

11.2.1 Hardening #

Three properties make invocation safe against a package trying to influence it.

A fixed absolute path. The set is closed, so peipkg knows each tool's location and never searches a path variable. A package cannot shadow the intended tool, and because the location is a root-level runtime view, populating the writable stratum behind it needs separate local-administrator authority that a package does not have.

A cleared environment. Each tool runs with exactly LC_ALL=C and PATH=/bin. Nothing is inherited from the invoking context, which closes the environment-injection route.

Standard input closed. Each tool runs with its input attached to the null device.

Output is captured and length-capped, so a runaway tool cannot flood the operation report.

11.2.2 The kernel release #

depmod names the release it indexes, and runs once per release whose module set the transaction changed. The releases come from the transaction's own file lists — every staged package's payload and every removal's ownership rows, under usr/lib/modules/<release>/ — rather than from the running kernel, so installing modules for a kernel other than the one booted (the normal case during a kernel update, and always the case in an image build) indexes the release that changed. A package shipping modules for two releases gets two invocations; a release split across two packages gets one.

A transaction that declares depmod but neither installs nor removes a kernel module runs nothing and reports a warning, rather than falling back to indexing the running kernel.

11.2.3 The root #

The tools invoked are always the host's binaries, at the host's fixed absolute paths, and each is directed at the root the transaction acted on. For the host root the bare forms in the table run. For an alternate root — a named root, an initramfs image, a mounted target — each root of a cross-root transaction schedules its own side effects against itself.

depmod is given the root with -b and the module directory beneath it with -m /usr/lib/modules. The second argument is what makes the first work: kmod's default module directory is /lib/modules, which on a running Peios is the runtime view of /usr/lib/modules, but an alternate root is storage with no views mounted over it, so without -m the tool would look under <root>/lib/modules and index nothing.

man-db is not run against an alternate root. The man index is a cache that the reading system's own man-db configuration locates and keys, and the host's mandb cannot be pointed at another root's configuration. The operation report carries one warning naming the effect and the root; page lookup in that root falls back to a filesystem scan until that system's own next man-db side effect rebuilds the index.

peipkg-compose runs no side effects at all, so a composed root's caches are never built by the composer either.

11.3 Timing and Failure

Peios / Peios technical reference / peipkg / Side Effects

11.3.1 Timing #

Side effects run at transaction commit, after the database commit, after every package in the transaction has completed extraction and registration.

They are deduplicated across the transaction: several packages declaring man-db produce one invocation. Distinct effects run in an unspecified order, which is safe because the recognised set is chosen so that order between them does not matter.

Running once, at the end, is what keeps the system consistent during a multi-package install: the man-page index is rebuilt after every package's pages are in place, not after each package individually against a partial state.

11.3.2 Failure #

Side effects run after the durability boundary, so a failure cannot roll the transaction back — the transaction is already committed.

A failure becomes a warning on the operation report and the transaction stands. The operation exits successfully: the packages installed, and only a cache lagged.

Because side effects are idempotent, a failed one is self-correcting. Re-invoking it — explicitly, or as part of the next transaction that declares it — reaches the correct state.

Note

Rolling a committed transaction back because a cache rebuild exited non-zero would be disproportionate. The two recognised tools are stable with few failure modes outside system-level corruption, and every one of those failure modes is recoverable by running the tool again.

11.3.3 Removals schedule too #

A side effect runs when a transaction removes files whose absence affects its target, as well as when it adds them. Removing the last package that owned a kernel release's modules reindexes that release; removing a package that shipped man pages reindexes the man database.

A removal declares nothing at the time — it has no incoming manifest — so the declaration is read from the manifest stored for the package being removed, and depmod's affected release comes from that package's ownership rows rather than from a payload.

An upgrade schedules effects implied by the files it removed as well as those the new version declares.

12.1 The Recipe Family

Peios / Peios technical reference / peipkg / Producing Packages

A recipe describes how to turn an upstream source tree into one or more packages. It is not a single file: pekit reads a family of TOML documents, each with its own role.

FileRole
pekit.tomlThe recipe: source acquisition, and the build, test, install, clean, and gen targets
workspace.pekit.tomlWorkspace marker: member globs, distro-wide environment, derivation policy
package.pekit.tomlShared package metadata — the base layer
<selector>.package.pekit.tomlOne emitted package per file
packages.pekit/An alternative directory holding the two above
env.pekit.toml, <name>.env.pekit.tomlA build entry's environment, wrapper, and dependency provider
<name>.keyring.pekit.tomlA secrets tree exported into the build environment
pekit.lockMachine-written source pin
<patches>/seriesThe patch series, plain text rather than TOML

12.1.1 Strict parsing everywhere #

pekit rejects an unknown key at every level, including the top level of every file, and every sub-parser carries its own closed key set.

There is no owner partition and no tolerance for a section pekit does not recognise, because there is only one tool reading these files. Adding a section a future version might own makes the recipe fail to load today.

12.1.2 The layer merge #

A package's definition is assembled from up to five layers, in order: the workspace base, the fetched source tree's base, the recipe's base, the source tree's member file, and the recipe's member file.

A scalar from a later layer overrides an earlier one when it is non-empty. A map or a slice replaces the earlier one wholesale — it does not merge key by key.

That rule has a consequence worth knowing when a base layer declares a dependency's root. The dependency constraints and the dependency roots are two separate maps built from one table, and each is replaced only when the overriding layer's own map is non-empty. A member layer overriding a dependency with the plain constraint form leaves the roots map untouched, so the base layer's root survives and is applied to the new constraint. There is no way to un-set a root a base layer declared.

12.2 The Recipe

Peios / Peios technical reference / peipkg / Producing Packages

pekit.toml has ten top-level keys.

KeyTypeMeaning
out_dirstringStage and output base, relative to the recipe root. Defaults to out
envtableEnvironment for target commands
wraptableA command wrapper applied to every target command
sourcetableWhere the source comes from
delegatebool or tableBorrow build targets, environment, wrapper, or package definitions from the fetched source tree
build, test, install, cleantablesTarget namespaces
gentableGeneration targets, with their own verification
source_packagetableWhether and under what name to emit a corresponding-source package

12.2.1 Environment #

[env] maps variable names to values. A name matches the usual shell identifier pattern, and cannot begin with the reserved prefix pekit uses for its own variables.

Declaration order is preserved, so a later variable may reference an earlier one — CXXFLAGS = "$CFLAGS ..." works because CFLAGS was declared above it.

12.2.2 Wrapping #

[wrap].command is a shell string or an argv array containing exactly one {{command}} placeholder. In argv form the placeholder is a whole argument, and cannot be the program name.

Every target command runs through the wrapper, which is how a whole recipe is built inside a sandbox or under a cross-compilation shim without each target knowing about it.

12.2.3 Targets #

A target namespace takes one of two shapes. A bare namespace — the table itself carries a command — is a single target named main. Otherwise each sub-table is a named target. Mixing the two is an error.

KeyMeaning
commandRequired. A shell string, or a non-empty argv array
needsOther targets that run first
clear_outWhether to clear the target's stage directory first. Defaults to true
dependenciesBuild-tool dependencies, in the build namespace only

Build-tool dependencies are grouped by provider — peipkg, apt, and so on — and each entry maps a capability name to a constraint string. The name is validated as a capability, so sonames and pkg-config module names are legal; the constraint is a non-empty string, with * meaning any.

These are the tools the build needs, and they drive the environment the build runs in. They are not the dependencies the resulting package declares.

12.2.4 Generation targets #

A gen target has its own shape: no needs, no clear_out, and a verification half.

KeyMeaning
commandRequired. Regenerates the artifact
verify_commandChecks that the committed artifact is up to date
verify_on_build, verify_on_testWhich targets in that namespace the verification gates
dependencies, verify_dependenciesAs for a build target; the verify set, when present, replaces rather than extends

The two gating keys are three-valued. Absent gates every target in that namespace; an empty array gates none; a populated array gates exactly those named. Setting either without a verify_command is an error.

This is how a generated file — an ABI table, a constants header — is kept honest: the artifact is committed, and a build fails if regenerating it would change it.

12.3 Sources

Peios / Peios technical reference / peipkg / Producing Packages

[source] says where the upstream tree comes from. At most one reproducible source may be declared — a version-control source or a fetched artifact — and declaring both is an error.

12.3.1 Version control #

KeyMeaning
urlRequired
refTemplated, defaulting to the version placeholder
versionsA constraint capping which upstream tags are eligible
tag_regexA pattern filtering tags during version enumeration

12.3.2 A fetched artifact #

KeyMeaning
urlRequired, templated
extractWhether to unpack the artifact
rootThe subdirectory of the unpacked tree that is the source root
versionsA constraint cap
file_regexA pattern applied to a directory listing to enumerate versions; named version-component captures can map upstream filenames to canonical package versions
checksumA single hash, or a table mapping version to hash

12.3.2.1 Signature verification #

A [source.url.signature] block makes upstream signature verification mandatory for that source.

KeyMeaning
urlTemplated; defaults to the source URL with a signature suffix
ofartifact or decompressed — which bytes the signature covers
key_filesRequired and non-empty: the pinned public keys
fingerprintsAn allowlist of acceptable signing fingerprints
ignore_expiryBoolean, default false. Permit new signatures made after the pinned key expired; use only for an upstream known to continue signing with that key

This is an entirely separate trust system from package signing. It verifies that the upstream tarball is the one upstream published, using upstream's own keys, pinned per recipe. It has no relationship to the Ed25519 signature the resulting package carries.

ignore_expiry bypasses only signing-key expiry. Pekit still verifies the cryptographic signature, pinned fingerprint, present-time revocation state, explicit signature lifetime, and signature timestamp.

12.3.3 A local tree #

[source.local].path names a directory relative to the recipe root. It is not a reproducible source and cannot be locked.

12.3.4 Patches #

source.patches names a single bare directory in the recipe root containing a series file: a plain-text list of patches, applied in order. It requires a reproducible source, since patching a local tree in place would mutate the operator's own working copy.

12.3.5 The lock #

pekit.lock records what was actually fetched: for each source, the version, the URL and content hash or the ref and commit, the signing key that verified it, and when it was locked.

It is trust-on-first-use and tamper-evident afterwards: the first fetch establishes the pin, and every later fetch is checked against it.

12.3.6 Delegation #

A recipe may delegate — declare that its build targets, environment, wrapper, or package definitions come from the fetched source tree rather than from the recipe. A delegating recipe is a thin pointer at a project that carries its own packaging, and it is how a project whose source tree already contains package definitions is distributed without duplicating them.

12.4 Package Files

Peios / Peios technical reference / peipkg / Producing Packages

Each emitted package is described by its own file, layered over a shared base.

KeyMeaning
formattar (the default) or peipkg
clear_outWhether to clear the package's stage directory first
buildsWhich build targets to stage; inferred from the file references when omitted
packageThe metadata table
filesSource reference to destination
symlinksDestination to target
excludesPatterns removed from the matched set
multipackFan this one definition into several packages
publishWhere to publish the result
dependencies, optional_dependencies, conflicts, provides, replacesThe manifest relationships
side_effectsThe declared maintenance operations
sd_overridesPath to security descriptor
claimsRole declarations

The manifest-facing lists are top-level tables in the package file rather than members of the metadata table.

A peipkg-format package additionally requires a version, an architecture, and a license — the last a distribution requirement stricter than the format's, which treats the license field as optional.

12.4.1 Metadata #

[package] carries the name, version, architecture, description, license, homepage, default_root, and special_system_package.

default_root is validated against the named-root grammar. special_system_package waives the layout check at pack time — but not the side-effect check — and grants nothing at install or compose time.

12.4.2 Claims #

[claims.provides.<role>.<slot>] and [claims.dependencies.<role>.<slot>] are checked at recipe load, and the two sides are not interchangeable: a provides slot names the target this package ships and may add a default path; a dependencies slot names only the path the role is reached at. peipkg enforces that asymmetry at install, so accepting the wrong shape here would mean a successful build, a signed package, and a failure on somebody else's machine.

A [claims.*] stanza naming a role the package neither provides nor depends on is an error. Claims are attached by role name, so a mistyped role would otherwise simply not appear in the shipped manifest, with no diagnostic — the one place a typo escaped pekit's unknown-key rejection, because the key is a role name rather than a schema field.

12.4.3 Field rules #

Pack encodes the manifest and decodes it back through the consumer's own validators before emitting anything, so a package violating a §5.18 field rule — the name grammar, the version form, the printable-ASCII description, the array limits — fails at the farm rather than at each consumer's install. The farm has the recipe, the maintainer and a fast feedback loop; the install has none of them.

12.4.4 Relationships #

Dependencies take either of two forms:

libfoo = ">= 1.2"
libfoo = { constraint = ">= 1.2", root = "initramfs" }

A table with no constraint matches any version. The table form is the only way to place a dependency in another root; there is no string sugar for it.

Conflicts, provides, and replaces are plain name-to-string maps. Conflicts deliberately carry no root: a conflict is root-local by construction, and the consumer rejects a root on a conflicts entry outright.

side_effects is a list of strings passed through verbatim: pekit does not check membership of the recognised set, the consumer does. What pekit does check is agreement with the payload, at pack time and in both directions — see Building and signing.

12.4.5 Files #

[files] maps a source reference to a destination. The key grammar is the interesting half:

ReferenceMeans
@recipe:<path>A literal tree in the recipe directory
@source:<path>A literal tree in the fetched source
@workspace:<path>A literal tree in the workspace
<target>:<path>The staged output of a build target
:<path>The staged output of the main target
<path>Rewritten to the owning layer's prefix

Globs are supported, and a directory source maps its whole subtree.

A value may be a plain destination string, or a table with a path and an override flag. override is not a merge flag: it excludes that one entry from the layout validation, per file, invisibly in the resulting package.

That is a second producer-side waiver alongside special_system_package — narrower, but undeclared in the artifact, so a consumer sees an ordinary package that simply fails validation at install time.

12.4.6 Symlinks and excludes #

[symlinks] maps a destination to its target, with the same override option. excludes is a list of reference-grammar patterns removed from whatever the file map matched.

12.4.7 Multipack #

[multipack].enum fans one package definition into several. It takes a static list of values, or a derived form naming a path and a pattern to enumerate from. Each value binds a placeholder available throughout the definition, so one stanza can emit a package per kernel module, per locale, or per plugin.

12.4.8 Publish #

[publish.localdir] is an array of tables naming a path and whether to overwrite. It is the local development route; a real repository is published with the repository tool.

12.5 Templating and Derivation

Peios / Peios technical reference / peipkg / Producing Packages

Two mechanisms mean the shipped manifest is not simply what the recipe wrote.

12.5.1 Templating #

Placeholders are substituted throughout a package definition: {{version}}, {{major}}, {{minor}}, {{patch}}, {{suffix}}, {{prerelease}}, {{buildmeta}}, and {{multipack}}.

They apply to the metadata scalars, to the keys and values of every relationship map, to side effects, to claim paths and targets, to file references and destinations, to symlink targets, to publish paths, and to the source ref and URL.

They deliberately do not apply to a dependency's root, which names a registered root rather than something derived from a version.

The components come from pekit's own upstream version model, not from the package version model. For a package version carrying a Peios revision, the revision lands in {{prerelease}}. An upstream's unseparated lettered release suffix, such as the c in IANA tzdata 2026c, lands in {{suffix}} instead.

A version the model does not parse — one carrying an epoch or a tilde — has no components at all, and every token but {{version}} is then refused with an error naming the version. {{version}} still renders, from the raw string, so a definition that uses only it is unaffected. Substituting an empty string instead produced a silently wrong result: a publish destination missing its major number, with no diagnostic anywhere.

The idiom for "this package depends on its sibling at this build's version" is a templated constraint:

libpeios = "{{version}}"

which pins the upstream version and leaves the revision unconstrained.

12.5.2 Derivation #

pekit derives capabilities from the built payload and merges them on top of what the recipe declared. A hand-written entry always wins.

Shared libraries. Sonames are read from the built objects: a library's own soname becomes a provide, and a binary's needed sonames become dependencies. Symlinks are skipped, so a versioned library and its development link do not both claim the same soname. A shared-library-shaped file carrying no soname produces a warning.

Where the workspace's symbol-version policy names a soname and a token prefix, the symbol versions a binary actually references become a version floor on that soname's dependency — so a binary using a recent C library symbol depends on a library version that has it.

pkg-config modules. Each .pc file becomes a provide named for the module, versioned from its version field. Its required modules become dependencies with ordered constraints, merged from both the public and private requirement lists. Variable references are expanded to a fixed point. An unparseable version or constraint is dropped with a warning rather than failing the build, and modules the package provides itself are subtracted from what it requires.

The consequence to hold onto: a shipped manifest routinely declares dependencies the recipe never wrote. Reading a recipe tells you what was declared, not what was shipped.

12.6 Building and Signing

Peios / Peios technical reference / peipkg / Producing Packages

12.6.1 The build #

pekit fetches or updates the source, applies the patch series, and runs the targets a package's builds list names, each into its own stage directory, honouring the dependency edges between them.

Every command runs with the assembled environment: pekit's managed variables, then any explicitly granted keyring values, then the user layers in order (the workspace, its selected environment file, the delegated source and its environment file, the recipe, and the recipe's environment file). In an isolated workspace every target runs in the sandbox the selected environment prepares, and no wrapper can replace it. See Environments and keyrings for the complete rules.

Generation targets run their verification where the gating keys direct, so a build fails if a committed generated artifact is stale.

12.6.2 Packing #

Packing collects the file map's matches from the stage directories, adds the symlinks, subtracts the excludes, applies templating, merges derived capabilities over declared ones, and hands the result to the packing library.

That library builds the manifest, the files manifest, and the archive according to PSPU §5, and then decodes its own output through the consumer's validators. A package that packs has already satisfied the rules a consumer applies on the way in — which is why a recipe error often surfaces as a manifest error at pack time.

The layout check runs here, over the whole file map minus any entry marked as an override.

A regular file of 8 GiB or more is refused before anything is hashed or written, naming the file and its size. The container has no representation for it: the ustar header's size field tops out at 8 GiB − 1 and §5.11 forbids the PAX size record that would extend it, and a consumer's 4 GiB decompression bound (§5.27) would refuse the package in any case. The size is the one recorded when the file map is walked, the same figure the tar header is written from.

The side-effect check runs here too, over the whole file map including overrides: an override escapes the layout rules, but a kernel module still needs indexing wherever it was declared. It enforces §5.24 in both directions for the effects whose trigger is a payload file pattern.

PayloadDeclarationResult
A .ko or .ko.* under usr/lib/modules/no depmodError
No kernel moduledepmod declaredError
A file under usr/share/man/no man-dbWarning
No man pageman-db declaredWarning

depmod is an error because §5.24 makes it a MUST and the failure it prevents is silent: a stale modules.dep makes modprobe resolve a dependency chain and then fail on a file that is not there, far from the package that caused it. man-db is a warning because §5.24 makes it a SHOULD — lookup falls back to a filesystem scan, which is suboptimal rather than broken.

Warnings are reported even when an error is also raised, so one run tells the author everything.

A side effect whose trigger is not a payload pattern is simply not in the checkable set. The rule is where the trigger is mechanical, pack enforces it, which leaves room for a future effect that depends on what a package means rather than on what it contains — the declaration stays the author's, and pack validates it rather than deriving it.

special_system_package does not waive this check, unlike the layout one. Special packages stage exotic layouts, which is why those rules let them through; what maintenance a payload needs afterwards is a separate question, and the kernel's module tree is exactly the payload that most needs depmod.

12.6.3 Signing #

The signing key is supplied through the keyring, under a well-known key name. The signature is computed over the uncompressed tar bytes preceding the signature entry and written as the archive's last entry, before compression.

A package built with no signing key configured is a conformant unsigned package, installable only from a repository whose policy permits unsigned content.

Private keys are read as raw key bytes or in a standard encrypted-key container. Neither encoding is specified by the format, which cares only about the resulting signature — but both are a real interface, since the keyring names a file that some other tool may have produced.

12.6.4 Binary signing #

Separate from the package signature is the signature the kernel checks on individual files — the ML-DSA-65 blob of PSPK chapter 3 that gives a binary its PIP tier and lets firmware reach a device. A build target declares which of its outputs get one and with which key:

[build.main.sign.pip]
"usr/bin/peinit" = "tcb.priv"
"usr/lib/firmware/**" = "tcb.priv"

Each entry maps an output-relative path or glob to the keyring leaf naming the private key. Signing runs after the build command, over the staged output, before packing. Every failure is fatal: a pattern that matches nothing, a key that does not load, a file matched by two patterns naming different keys, or a signed file that does not verify back through an independent check.

Where the signature lands depends on the file. An ELF file gets a .peios.sig section, reserved and zero-filled before hashing so the bytes the kernel hashes are the bytes that were signed. Any other file — a firmware blob, a data file — gets a detached signature, <file>.peios.sig, written beside it with the blob over the whole file. The file is untouched; the sidecar is packed as an ordinary payload entry and peipkg turns it into the security.peios.sig extended attribute at install (PSPU §5.16). A glob that sweeps a sidecar back up does not sign the signature, so ** is safe.

Each signed file produces one sign event naming the placement — section or sidecar — the keyring entry and the key's fingerprint.

12.6.5 Corresponding source #

For any recipe with a reproducible source producing packages in the package format, pekit emits a corresponding-source package by default: the pristine upstream artifact, the applied patch series, and the build-controlling recipe files, laid out under the source destination.

The emitted package's name is recorded in the built package's manifest, so a consumer holding a binary can find the source that produced it.

A recipe turns this off with [source_package] enabled = false, and the only reason to do so is that there is no source: the upstream artifact is itself the thing shipped, so a corresponding-source package would be that artifact again under another name. Device firmware is the standing example — the blobs are what the vendor publishes, there is nothing they were built from that Peios could ship, and the firmware packages carry license_class = "firmware" for the same reason. A recipe that does build something from an upstream source keeps the default, whatever the licence: the corresponding-source package is how a binary's provenance is answered, not a statement about the licence's terms.

12.7 Reproducibility

Peios / Peios technical reference / peipkg / Producing Packages

A package is reproducible when the same inputs produce byte-identical output. The format's determinism rules (PSPU §5.11) are necessary but not sufficient: they constrain what the archive looks like, not how the producer arrived at its contents.

12.7.1 What the format fixes #

Entry ordering, modification times tied to the recorded build timestamp, uniform ownership and mode, no extended attributes, canonical extended header records, and a fixed header format. Given the same uncompressed tar stream and the same key, the signature is deterministic too.

12.7.2 What it does not fix #

Compression. The level, the implementation, its version, and its frame parameters all change the resulting bytes and none is constrained. pekit pins its own choice, so pekit reproduces pekit; two producers seeking byte-identical output have to agree on compression out of band.

Manifest serialisation. The manifest's bytes are inside the archive that gets hashed and signed, so two semantically identical manifests with different whitespace produce different packages. pekit's serialisation is compact, unescaped, with a single trailing newline, with fields in schema order and a fixed rule about which optional fields are always emitted. That is pinned in the producer rather than in the format.

12.7.3 What the build has to control #

Everything the build process can observe: timestamps, file ordering, locale, environment, ambient filesystem state, and build paths.

The established techniques apply. SOURCE_DATE_EPOCH gives every build tool one timestamp to stamp its outputs with. A sealed environment — a container or a virtual machine — pins the full build dependency closure, which is itself a build input that determines the output. LC_ALL=C and TZ=UTC suppress locale-dependent ordering and time formatting. And build-path normalisation keeps absolute paths out of debug information and out of anything else a compiler embeds.

12.7.4 Verifying it #

The manifest records the build's farm identifier, its source reference, optionally the recipe tree's version-control identity and the producing tool's revision, and the timestamp. A third party with the same inputs can re-run the build and compare the output bytes.

The format supplies the inputs for that verification and does not mandate it.

12.7.5 Before publishing #

Worth checking before a package is published: that its hash matches what was recorded, that its signature verifies against the publishing key, that it installs cleanly in a fresh environment, that its dependency declarations reference packages that exist and constraints that are satisfiable, and that re-running the build from the same inputs produces identical bytes.

The cost of catching an error before publication is low; the cost of a published incorrect package is re-publication and a user-facing rollback.

13.1 The Privilege Model

Peios / Peios technical reference / peipkg / Security

peipkg holds no principal, no identity, and no access rights of its own.

It runs as the calling operator. Every file operation it performs — creating, replacing, or deleting — is checked by the kernel against the caller's token and the target's security descriptor. If the caller is authorised the operation succeeds; if not, it fails. peipkg contributes no authority.

This is verifiable in the shape of the program. There is no daemon, no broker, and no socket. Nothing in it changes identity: no user or group is assumed or dropped, no capability is manipulated, no ownership is changed on any file it writes. No privilege is requested anywhere. The only privilege its operation touches is the one audit emission consumes passively (§13.3).

13.1.1 What follows #

The authority to install a package is exactly the authority to write the directories the package installs into — an ordinary matter of security descriptors on the permitted destinations. A deployment grants installation authority by granting those write rights to whichever principals it intends to be able to install software.

A package's declared security descriptor overrides can only assign descriptors the calling operator already has the authority to assign. Where applying a given descriptor requires a particular right — WRITE_DAC for a discretionary access list, WRITE_OWNER and SeRestorePrivilege for an owner assignment — that right is held by the operator, not by peipkg. A package cannot, through peipkg, obtain authority the operator running it does not hold.

That consequence is currently theoretical rather than load-bearing, because overrides are parsed and then never applied (§5.4).

Note

An earlier design ran the package manager as a dedicated service principal holding broad system rights — a posture comparable to a root process. Peios instead makes it an ordinary unprivileged program: there is no standing principal to compromise, no privilege to confine, and the blast radius of a malicious package is bounded by the authority of whoever ran it.

The cost is that scoped, curated installation — letting a low-authority operator install one approved package without granting general write access — is not something peipkg can provide. That is the job of the higher-level roles and features layer, which can act as a privileged broker.

13.2 Blast Radius

Peios / Peios technical reference / peipkg / Security

Because peipkg holds no principal of its own, there is no standing privileged identity to confine and no privileged process to compromise. The authority exercised during an install is the caller's, and only for the duration of that invocation.

The blast radius of installing a malicious or defective package is therefore bounded, precisely, by the authority of the operator who installed it: a malicious package can do nothing the operator could not already do directly.

That makes the trust decision — which repositories an operator configures, and which packages an operator with broad authority chooses to install — the operative security boundary. Signature verification and the repository trust model exist to inform that decision. They do not substitute for it.

13.2.1 What peipkg does not do #

peipkg needs write access to the payload destinations and network access to fetch from configured repositories, and nothing beyond that.

  • It performs no kexec.
  • It loads no kernel modules. The depmod side effect rebuilds a module dependency map; it does not load anything.
  • It loads no BPF programs.
  • It writes no registry state. Its bookkeeping is a private store, and the database schema says so in as many words.

The one thing that reaches outside its own state is a side effect, and that is a fixed absolute path, with a cleared environment, from a closed set of three (§11.2).

Note

peipkg has its own thing it calls a registry: the table recording which named roots exist. That is a table inside the private store, not the system registry, and the claim above is about the latter.

13.3 Audit

Peios / Peios technical reference / peipkg / Security

Every package peipkg installs, upgrades or uninstalls writes an audit record, and so does every repository add, removal and refresh, every claim grant or revoke, every run of peipkg recover, and every elevated action put to the operator. Appendix A2 lists the types and their fields; the records are defined in peipkg's evman fragment, installed as /usr/share/evman/peipkg.evman.

Events go into the kernel event subsystem, KMES, through the kmes_emit system call. Because emission is a local kernel call rather than a message to a userspace daemon, it has no unreachable-destination failure mode: there is no reachability probe, no fail-closed rule, and no retention journal. Whether and when events are drained and persisted is the historian's concern, not peipkg's.

13.3.1 What a record carries #

FieldWhere it lives
OperationThe event's type
TimeThe kernel-stamped header
The caller's token, true token and processThe kernel-stamped header, as GUIDs
The acting principalThe payload: subject.token.sid
The packageThe payload: name, version, previous version, architecture
Source repositoryThe payload: source.repository.name
Transaction identifierThe payload: transaction.id
Outcome, with an error code and the error textThe payload

A record's fields follow the platform's event conventions: a field path is a chain of nested maps, and a value that does not apply is left out rather than written as an empty string or zero. A removal carries no architecture or source repository, a package installed from a local file carries no repository, and a request refused before any transaction opened carries no transaction identifier.

The acting principal is the user SID of the token peipkg runs under, read from that token. It is peipkg's own word, written so that "who installed this" can be answered from the record alone. The kernel also stamps the caller's effective token, its true token and its process identity onto every emission, as GUIDs, where they cannot be forged or suppressed by the emitting program. If peipkg cannot read its own token, the record is still written without the SID, and peipkg warns.

One record per package. A transaction writes one record for each package it touched, typed by what happened to that package rather than by the command: an upgrade that pulls in a new dependency writes peipkg.package.installed for the dependency, and undoing an install writes peipkg.package.uninstalled. A downgrade and an undo that moves a package backward are recorded as peipkg.package.upgraded; the record's two versions show the direction.

The source repository is per package, because one plan can draw from several repositories. It is how a bad install is traced back to the repository that served it.

The transaction identifier joins a record to the transaction ledger. In a cross-root transaction each package's record carries the transaction of the root the package went into, so every record joins to its own root's ledger.

13.3.2 Successes and failures #

A committed transaction writes one successful record per package. One that fails writes one failed record per package its plan held, with outcome.reason set to the same stable error code the driven mode reports (stale, busy, denied, unowned, alternate-upgrade, unresolvable, untrusted or failed) and the error text in outcome.detail. A failure that carries a transaction identifier was rolled back; one without was refused before it began. A request refused before a plan existed writes one failed record for each package it named, and an upgrade of everything, which names none, writes one record without a package name.

A failed repository add or removal, and a failed peipkg recover, are recorded as failed. An operator who declines an elevated action is recorded too, as peipkg.action.authorised with a false outcome.

An operator who declines the routine proceed prompt writes nothing: the transaction never started.

An automatic recovery at the head of an ordinary operation writes nothing. The same rollback performed deliberately through the recover command does, and so does every run of it, one with nothing to recover included.

peipkg-compose writes nothing at all.

13.3.3 Tiers and the emission policy #

The package records, the repository add, removal and reconfiguration records, and the authorisation record are essential: they are always written. peipkg.claim.changed, peipkg.repository.refreshed and peipkg.transaction.recovered are standard, so the system's emission policy can switch them off; peipkg consults it before building their payload.

13.3.4 What emission depends on #

Emitting requires an audit privilege on the caller's token. peipkg warns and continues when emission fails, so an operator with write access to the payload destinations but without that privilege installs packages with no peipkg audit record, essential types included.

On a kernel without the emit call at all, emission is silently treated as a successful no-op, so "audit is working" and "audit is absent" look the same.

Note

peipkg's own events are a semantically meaningful summary of an operation. They are not the security boundary. The authoritative record of what changed on disk is the kernel's own audit of the underlying file operations, which the calling operator cannot suppress. A forged or omitted peipkg event cannot conceal a file operation from the kernel's record — which is why the gaps above are gaps in diagnosis, not in accountability.

13.4 Operator Authorisation

Peios / Peios technical reference / peipkg / Security

Several points call for operator authorisation: a deliberate, explicit act specific to the elevated action in question, distinct from the routine prompt to proceed, never inferred or defaulted, and recorded in the audit stream.

13.4.1 What is gated #

Elevated actionGate
DowngradeA per-action prompt, audited
A low-trust provider filling a high-trust roleA per-action prompt, audited
A foreign replaces against a higher-priority packageA per-action prompt, audited
Proceeding on stale trust stateThe --allow-stale flag, audited, no prompt
Installing unsigned content under an optional policyWarned per package on every operation; not gated, not audited
Enabling insecure transportNot gated, not audited
Resolving an interrupted transactionNot gated, not audited

The three prompted actions are raised by the resolver as authorizations, presented individually, and confirmed on their own terms. The authorising act and what it authorised are recorded.

13.4.2 --yes does not satisfy them #

--yes confirms the routine "apply this plan?" prompt. Authorizations are collected and confirmed before that prompt is reached, so --yes satisfies none of them.

With input closed, an authorization prompt reads end-of-input and returns a refusal, so a non-interactive invocation of an elevated action cancels rather than proceeding. That is the right direction to fail.

13.4.3 Confirming a specific action #

The text interface displays the action's detail and requires a phrase naming both the action and package. For example, downgrading widget requires authorise downgrade widget, followed by a newline. The other action names are low-trust-provides and foreign-replaces. Use the phrase printed by the prompt; matching is case-sensitive, with surrounding whitespace ignored.

A generic y or yes refuses an elevated action. The first refusal cancels the plan, and each answer given is audited. End-of-input, including an unterminated phrase, also refuses. --yes still skips only the routine proceed prompt.

Scripts may supply these specific phrases through standard input; a terminal is not required. This prevents a generic stream of affirmatives from approving every gate, but is not proof of a human's presence or a cryptographic authorisation. The structured driven interface continues to accept a separate yes answer tied to each authorisation question's ID.

13.4.4 Flags outside the frame #

Three flags waive a check with a bare boolean and no authorisation record: the path-restriction bypass, and — where they exist — a critical-package override and an unowned-file overwrite. The first touches the payload layout rules directly, and none of the three appears in any audit event.

13.4.5 Where this is going #

The intended end state binds authorisation cryptographically: a fresh, kernel-authenticated authorisation from a principal holding rights beyond the operator's routine set, carrying the transaction identifier, the full operation specification, a nonce, and a timestamp; validated by the kernel rather than by peipkg; and emitted as an audit record co-signed by the authorising principal, so the trail does not rest on peipkg's own honesty.

That depends on kernel primitives that do not exist yet — an asymmetric key bound to a token, and event-payload signature verification. Until they do, authorisation is the deliberate act described above, and peipkg does not present it as the stronger guarantee.

13.5 Trust Anchors

Peios / Peios technical reference / peipkg / Security

A trust anchor is a key fingerprint an operator supplies out of band, and it is the root of everything the repository trust model builds on (§3.2).

13.5.1 Where they come from #

Two places, and only two: the --anchor option at repository-add time, and the trust-anchors key of a repository's configuration file.

The intended third place — a file installed by the base system, outside any package, carrying the official repository's anchors — does not exist, and nothing looks for one.

13.5.2 What that means #

The configuration directory those files live in is the writable local tier. So the official repository's anchors, when an image ships them, sit in mutable local state protected by that directory's security descriptor rather than in a read-only base-system location.

The configured form of repo add performs the full trust ceremony against anchors read from that directory. Anything able to write there can substitute the official repository's anchors before the ceremony runs.

Placing a rogue repository configuration is not by itself an escalation — adding a repository is an operator action, and the security descriptor on the directory is what decides who may take it. Substituting the anchors of a repository the operator believes is already trusted is a different thing.

13.5.3 The redundancy that is planned #

The intended end state distributes anchors redundantly: the base-system file, plus a registry key populated at first boot, with peipkg cross-checking every available source byte for byte, refusing repository operations on disagreement — with the base-system file authoritative — and refusing on a missing source in a rescue-media boot.

The model is additive: once the registry source exists, the cross-check applies without changing the file's role as authoritative.

Neither leg is present today. The registry source is correctly inert, since the registry it would live in is not yet available. The base-system file it would be checked against is simply absent.

13.6 Clock Dependence

Peios / Peios technical reference / peipkg / Security

Several checks depend on the local system clock: a signing key's validity window, the maximum trusted age, index staleness, and build provenance timestamps.

An attacker able to manipulate the local clock can extend a transitioning key's validity, evade a staleness check, or hide a compromise-detection window.

peipkg does not gate on clock sanity, and does not pretend to. There is no build-timestamp comparison, no time-synchronisation state query, and no override flag for a clock peipkg thinks is wrong. The clock-dependent checks assume a sane clock.

Note

The intended end state refuses operations when the clock is plausibly wrong — when the current time precedes peipkg's own recorded build timestamp, or when no reliable time source has reported a successful synchronisation since boot. A reliable source means a cryptographically authenticated time protocol, or agreement among several independent unauthenticated ones; plain unauthenticated time synchronisation, being trivially substitutable by an attacker in a network position, does not on its own qualify.

Operators in environments where clock manipulation is a concern will want an authenticated time source in use.

13.7 Threats Out of Scope

Peios / Peios technical reference / peipkg / Security

The following are explicitly outside what the package manager defends against. Each is a real concern; each is addressed elsewhere.

Compromise of an installing operator's identity. peipkg runs with the operator's authority (§13.1). If that identity is compromised, no format-level defence helps. The kernel's own audit and recovery mechanisms apply.

Side-channel attacks on installed binaries. Speculative-execution, cache-timing, and similar attacks against installed software are the kernel's concern and the software's, not the package format's.

Physical attacks on storage. A physically compromised disk can have its package database or installed files altered offline. Hash verification at use time — outside the package manager — is the appropriate defence.

Compromise of the build farm. A compromised farm can sign malicious packages with trusted keys. Detection requires independent reproducible-build verification (§12.7).

The build farm identifier recorded in each manifest can be constrained per repository as defence in depth — refusing packages from a farm not on a configured allowlist. That is not implemented, and it would protect only against a stale farm whose key was rotated but whose identifier is still recognised, or against an attacker who obtained a key without the farm's operational identity. It is not a format-level guarantee.

Network-level censorship or denial of service preventing package fetching. An operational concern.

Note

These exclusions are honest acknowledgements rather than weaknesses to dismiss. What they have in common is that each sits either below the package manager — where the kernel is the right layer — or above it, where the operator's judgement about what to trust is the only thing that can help.

14.1 An Interrupted Transaction

Peios / Peios technical reference / peipkg / Failure Modes

The system loses power, or the process is killed, partway through an operation.

14.1.1 What survives #

The transaction's journal is rows in the package database, written before any file moved, carrying the backup map. The database is a transactional store, so the journal is either there or it is not — never half-written.

14.1.2 What happens next #

For an ordinary single-root transaction, the next install, upgrade, or uninstall acquires the lock, finds the pending transaction, and rolls it back: every displaced original renamed from its backup path back into place, every directory the transaction created removed, every staged file discarded, and the transaction cleared.

That happens automatically and silently. No prompt, and no audit event.

14.1.3 If the crash came after the commit #

The transaction is committed and there is nothing to recover. What is left is cleanup: staged files and backups that were never discarded.

Those are siblings of their destinations under names carrying the transaction identifier, and they are removed by the cleanup step whenever it next runs.

14.1.4 If the transaction spanned several roots #

The pinned cross-root recovery path rolls pending participants back if none committed. If a sibling committed, it rolls pending participants forward from persisted completion data; missing or malformed data needed for completion causes refusal. Recovery can itself fail and must not be treated as a whole-system rollback.

A pending cross-root transaction found by an ordinary single-root operation is refused in isolation. Run peipkg recover explicitly from the original anchor with the named-root topology intact and every participant reachable. Its console wording is not proof of recovery direction, and roll-forward does not retry normal post-commit maintenance. Follow the operator checks before treating the system as ready.

14.1.5 If peipkg was upgraded in between #

The journal records the schema version it was written under. A peipkg that can read it recovers the transaction; one that cannot refuses, naming the schema version, and leaves it for a version that can.

14.2 A Failed Rollback

Peios / Peios technical reference / peipkg / Failure Modes

The rollback itself fails: a write error, a filesystem gone read-only, a permission change mid-operation.

14.2.1 What is left #

Some originals restored, some still at their backup paths. Some new files removed, some still at their final paths. The database holding the pre-transaction state, because the commit never ran.

14.2.2 What peipkg does #

For the reviewed cross-root preparation rollback in peipkg 8b588ae8, it reports rollback errors and retains the affected pending journal. Use coordinated recovery, with all participants reachable from the original anchor.

Earlier wording here described errors being discarded and the journal closed as rolled back. That closed-journal failure remains a diagnostic case to distinguish; it is not the contract of the pinned cross-root path. This correction does not verify every single-root path or released image.

14.2.3 What that means for the next operation #

First inspect whether the journal is pending or closed. A pending cross-root transaction is not safely recovered in isolation by an ordinary single-root operation. Preserve its participants and recovery files.

In the previously described closed-journal case, recovery finds no pending transaction to retry even though restoration failed. A rolled-back record can then look authoritative while files still disagree with it. Neither case makes file verification optional.

14.2.4 The signal that is available #

peipkg verify re-hashes every recorded file against what is on disk. Files the rollback failed to restore will not match their recorded hashes, and will be reported as modified.

A directory listing around the affected paths shows the leftovers directly: siblings carrying the backup and staged markers with the failed transaction's identifier.

14.2.5 Getting back to a known state #

Retain staged and backup siblings while recovery and diagnosis still need them. Identify the affected paths, inspect transaction state, and verify what was restored before deciding whether old or new content is wanted. Follow the operator recovery checks for unresolved files and package records; do not delete recovery material or start another package change merely to make the leftovers disappear.

14.3 A File That Does Not Match

Peios / Peios technical reference / peipkg / Failure Modes

peipkg verify reports an installed file whose content no longer matches the hash recorded at install.

14.3.1 What it can mean #

A legitimate edit. Someone changed a configuration file, or patched a binary in place.

Corruption. Storage failure, or a rollback that did not complete (§14.2).

Tampering. Something modified an installed file.

Neither, for a preserved configuration file. An upgrade that preserved an operator's edit records the new version's hash against the original path while leaving the operator's content there. That file is reported as modified on every run afterwards, permanently, and nothing about it is wrong (§6.2).

14.3.2 Distinguishing them #

The fourth case is identifiable: a file under a configuration path with a .peipkg-new sibling is a preserved edit, and the sibling holds what the package shipped.

For the rest, the recorded hash is what the package's files manifest said at install, so re-downloading the package and comparing is conclusive about what the content should be.

14.3.3 Reinstalling #

There is no reinstall verb. Restoring a file to what its package shipped means removing the package and installing it again, in two transactions, or downgrading and upgrading across the same version.

14.4 An Unreachable Repository

Peios / Peios technical reference / peipkg / Failure Modes

14.4.1 During a refresh #

The fetch fails, the failure is reported, and the previous trust state is retained untouched. peipkg does not fall back to unverified state and does not silently proceed.

peipkg refresh reports each repository's failure and exits non-zero.

14.4.2 During an install #

If the repository's trust state is within its maximum age, the cached index is used and the operation proceeds normally.

If it has aged out, peipkg attempts a refresh first. A failed refresh — or a refresh that returns the same index it already had — refuses the operation, unless the operator passes --allow-stale.

Uninstall and undo are not gated this way, so removing something and reverting a change still work offline.

14.4.3 When the cache is unusable #

A cached index that fails to load or fails to verify produces a warning, and resolution continues without that repository.

The consequence is worth watching for: a package the operator expected from that repository is resolved from wherever else it is available, at a lower priority, with only a warning to say so.

14.4.4 When a repository has been removed #

Packages installed from it stay installed, and their recorded origin stays with them. They are not marked, not flagged in query output, and not refused for upgrade.

Because their origin no longer resolves to a configured repository, the cross-repository guards do not fire for them (§3.7).

14.5 Disk Exhaustion

Peios / Peios technical reference / peipkg / Failure Modes

peipkg checks free space before it stages anything, so exhaustion normally arrives as a refusal that costs nothing. A write can still fail — another process can consume the space in between, and the estimate is of payload rather than of the metadata the filesystem writes alongside it — and where in the operation that happens decides how bad it is.

14.5.1 During staging #

The cleanest case. Nothing has been renamed into place, the transaction rolls back, and the staged files are discarded — recovering the space they consumed.

14.5.2 During the apply phase #

Some files have been renamed in and some originals are sitting at their backup paths.

Because backups are renames rather than copies, the apply phase itself consumes almost no additional space: the space was consumed during staging. A failure here is more likely to be a different error than exhaustion.

Rollback restores from the backup map, which is again renames, so it does not need space either.

14.5.3 During the commit #

The database commit needs space for its own write-ahead log. A failure there leaves the transaction uncommitted, and rollback proceeds normally — but a database that cannot write is a database that cannot record a rollback, which is where §14.2 begins.

14.5.4 The check #

A transaction's requirement is the incoming file content of its added and replaced entries, aggregated across every operation. Backups cost nothing, because they are renames rather than copies, and a removal frees rather than consumes. A replaced file's old bytes are not subtracted: they are still on disk under the backup name until the commit succeeds, which is exactly when the space is needed.

The figure is computed per filesystem, not as a single total, because a transaction can span several — installation roots and destinations within a root can be separate mounts. Destinations are grouped by the filesystem id their directory reports, so the mount table never has to be read.

The comparison is against the blocks available to an unprivileged writer, not the free blocks: the reserved fraction is not peipkg's to spend. A 32 MiB margin is left on top, covering the inodes, directory entries, journal blocks and tail padding the filesystem writes that the payload figure does not include. The check runs before the journal is opened, so a refusal leaves nothing to recover from.

14.6 A Broken Claim

Peios / Peios technical reference / peipkg / Failure Modes

A claim path is missing, dangling, or pointing at the wrong thing.

14.6.1 Missing #

The role may simply be unheld — its holder was uninstalled and nothing was promoted (§9.7). peipkg claim <role> reports the holder and the remaining eligible providers, and granting to one of them restores the link.

The role may be held with no consumer declaring a path, in which case there is nothing to materialise and nothing is wrong.

Or a package payload may have been installed over the link. peipkg does not prevent that: the link is renamed aside, the payload file takes the path, and the claim-link record survives. Reconciliation then compares its record against its own desired set, finds them equal, and never notices. The link does not come back on its own.

14.6.2 Dangling #

The holder's target does not exist. Either the target names a path the holding package does not ship — which peipkg does not check at install time (§9.2) — or something removed it.

14.6.3 Pointing at the wrong thing #

A claim path cannot land outside the managed tree: the manifest decoder confines it to the permitted install destinations, /run/ and /init (§9.2). A new claim refuses differing unowned content at that path by default. If package installation was explicitly authorised to overwrite unowned content, the displaced file is retained and the warning names its backup path. An identical relative symlink is adopted without replacement.

For a link that is already recorded as manager-owned, a holder change still atomically repoints it. As described below, reconciliation compares database records rather than checking for manual changes to existing claim links.

14.6.4 Repairing #

Granting a role to a provider repoints every one of its links from current state, so a grant — even a grant to the current holder's alternative and back — is the blunt instrument that rebuilds a role's links.

Where the database's record disagrees with the filesystem, reconciliation will not detect it, because it diffs the recorded set against the desired set rather than against disk. Reinstalling the holder is what refreshes both.

Appendix A State and Paths

Peios / Peios technical reference / peipkg

A.1 Per installation root #

LocationContent
<root>/var/state/peipkg/db.sqliteThe package database: installed packages, owned files, repositories, claims, named roots, and the transaction journal (§2.5)
<root>/lcl/conf/peipkg/<name>.repoOne repository configuration file per configured repository
<root>/var/state/peipkg/cache/The index cache: content-addressed index and signature objects, and a pointer file naming the current object per repository

The database is the only authoritative state. The configuration directory is the operator's, and the cache is disposable.

A.2 Transient names #

PatternMeaning
<destination>.peipkg-staged-<txnid>A file written but not yet committed, a sibling of its destination
<destination>.peipkg-backup-<txnid>A displaced original, a sibling of its destination
<name>.peipkg-newA package's new default beside a configuration file the operator had edited

The first two are removed at commit or at rollback. The third is permanent and is owned by nothing.

Where a destination's basename is long enough that adding the marker would exceed the filesystem's name limit, the basename is truncated to fit.

A.3 Composition #

LocationContent
<manifest-stem>.lock.tomlThe pinned closure a composition resolves to
<out>.peipkg-compose-tmpThe tree under construction, renamed into place on success
<out>/usr/share/licenses.jsonThe license inventory the composer writes, owned by no package

A.4 Side-effect tools #

IdentifierHost root (/)Alternate root
depmod/libexec/depmod -a <release>, once per affected release/libexec/depmod -b <root> -m /usr/lib/modules -a <release>, once per affected release
man-db/bin/mandb -qSkipped with a warning; man-page lookup falls back to a filesystem scan

These are peipkg's invocations, not commands to copy as a manual recovery recipe. The release comes from the transaction's installed or removed module files, not the running kernel. If no affected release is found, depmod is skipped with a warning; peipkg does not fall back to bare depmod -a. ldconfig is not a recognised side effect.

See Invocation for root and release selection and Timing and Failure for post-commit warnings: a side-effect failure does not roll back an already committed package transaction.

The command forms follow peipkg 8b588ae8's side-effect implementation. Its execution path returns warnings after commit. These source forms do not establish which revision an installed image contains.

Each with a cleared environment of LC_ALL=C and PATH=/bin, and with input closed.

depmod sits in /libexec rather than /bin because it is machine-facing: this and the kernel's own make modules_install are its only callers, and no documented workflow has a person running it. The fixed-path property the design depends on is unaffected — /libexec is a merged view with the same local-stratum protection as /bin, so a package still cannot shadow the tool.

A.5 Producer files #

FileRole
pekit.tomlThe recipe
workspace.pekit.tomlThe workspace marker
package.pekit.toml, <selector>.package.pekit.tomlPackage definitions, layered
packages.pekit/An alternative directory for the above
env.pekit.toml, <name>.env.pekit.tomlBuild environments
<name>.keyring.pekit.tomlSecrets, including the package signing key
pekit.lockThe source pin
<patches>/seriesThe patch series

Appendix B Audit Events

Peios / Peios technical reference / peipkg

Every event described here is emitted into the kernel event subsystem (§13.3). Each type and field is defined in peipkg's evman fragment, installed as /usr/share/evman/peipkg.evman, which is the authoritative description; this appendix summarises it.

These types also appear in the Peios Events Index, a page per type, alongside every other event the system emits.

B.1 Types #

TypeTierWritten for
peipkg.package.installedessentialEach package a transaction installs, or a request to install one that failed
peipkg.package.upgradedessentialEach package a transaction moves to another version — upgrade, downgrade or undo — or a request to that failed
peipkg.package.uninstalledessentialEach package a transaction removes, or a request to remove one that failed
peipkg.repository.addedessentialA repository add and its trust ceremony, successful or failed
peipkg.repository.removedessentialA repository removal, successful or failed
peipkg.repository.reconfiguredessentialEach trust-relevant setting a repository add changed
peipkg.repository.refreshedstandardA refresh, successful or partly failed
peipkg.claim.changedstandardA claim grant or revoke, successful or failed
peipkg.transaction.recoveredstandardEvery run of peipkg recover, successful or failed
peipkg.action.authorisedessentialAn elevated action put to the operator, authorised or declined

An essential type is always written. A standard type is written unless the system's emission policy switches it off.

B.2 Payload fields #

Every record carries subject.token.sid, the user SID of the token peipkg ran under. A field that does not apply is absent.

FieldCarried byContent
transaction.idpackage, claimThe transaction; in a cross-root transaction, the transaction of the package's own root. Absent when none opened
object.package.namepackage, authorisedThe package
object.package.versionpackageThe version installed, moved to, or removed
object.package.version-previousupgradedThe version moved from
object.package.architectureinstalled, upgradedThe package's architecture
source.repository.nameinstalled, upgradedThe repository the package came from; absent for a local file
object.repository.namerepository, authorisedThe repository
object.repository.urladdedIts base URL
config.namereconfiguredThe setting, as the repository file names it
config.text, config.text-previousreconfiguredsignature_policy, new and old
config.value, config.value-previousreconfiguredAny other setting, new and old: days or an index version; 1 or 0 for allow_insecure_transport; the anchor count for trust_anchors
object.claim.role, object.claim.holderclaimThe role, and its new holder; no holder on a revoke
operation.succeeded-count, operation.failed-countrefreshedRepositories refreshed and failed
operation.succeeded-countrecoveredInterrupted transactions rolled back
operation.nameauthorisedlow-trust-provides, foreign-replaces, downgrade, remove-modified-file, stale-trust-state or stale-index
object.file.pathauthorisedThe modified file, for remove-modified-file
outcome.successall but reconfiguredWhether it succeeded; for authorised, whether the operator agreed
outcome.reasonpackage, claim, addedOn failure, peipkg's error code
outcome.detailall but reconfigured and refreshedOn failure, the error text; for authorised, the action as it was put

A failed package record with a transaction identifier was rolled back; one without was refused before any transaction opened. A cross-root transaction that fails while preparing its roots leaves no identifier on its records.

B.3 Where events do not appear #

  • Automatic recovery at the head of an ordinary operation writes nothing.
  • Declining the routine proceed prompt writes nothing.
  • A repository add refused before it names a base URL writes nothing.
  • A refresh that fails before reaching any repository writes nothing.
  • A stale repository refused because --allow-stale was not given writes no authorisation record; only proceeding with one does.
  • Enabling insecure transport, and installing unsigned content under an optional policy, write no authorisation record.
  • peipkg-compose writes nothing at all.

B.4 What emission depends on #

An audit privilege on the caller's token. Without it, emission fails, peipkg warns, and the operation proceeds unaudited, essential types included.

On a kernel with no emit call, emission is a silent successful no-op.

Appendix C Catalogue package families

Peios / Peios technical reference / peipkg

Use peipkg search to find what your configured repositories currently offer. This appendix records the package-family, integration and build details; the install procedure covers applying a change.

C.1 Canonical package names #

New Peios catalogue packages use canonical reverse-DNS names. The leading segments identify the upstream namespace and the final segment preserves the familiar package name, even when that deliberately repeats part of the namespace:

com.amd.amd-ucode
org.gnu.bash
org.sourceware.elfutils
org.peios.peinit

Peios-built packages of third-party software retain the upstream namespace; Peios-owned software and Peios-specific integration packages use org.peios. The name identifies the packaged software, while the repository signature and build provenance identify who packaged it. It does not imply that upstream signed or endorsed a downstream package.

Related payloads retain that canonical base. For example, org.sourceware.elfutils-libs carries libdw and libasm, org.sourceware.elfutils-libelf carries libelf, and org.sourceware.elfutils-devel carries their development interfaces without requiring the command-line tools to be installed.

The elfutils tools include command manuals and eu-make-debug-archive for preparing an offline debugging archive. To archive one executable, use eu-make-debug-archive debug.a -e /path/to/program; its debugging information must be available. The optional --sudo mode requires a separately installed and authorized privilege helper. The debuginfod service/client and stackprof components are not included in this family.

GNU C Library administration commands and their manuals are in org.gnu.glibc-bin. Profiling and tracing commands, including the Perl-based mtrace, are in org.gnu.glibc-utils. Extra legacy character converters are in org.gnu.glibc-gconv-extra, and the reference manual is in org.gnu.glibc-doc, as HTML under /usr/share/doc/org.gnu.glibc/html/. These utilities retain Peios's fixed authority and resolver providers; installing them does not make local passwd files an account authority.

Libtraceevent's library symbols are in org.kernel.libtraceevent-libs-debuginfo, while plugin symbols are in org.kernel.libtraceevent-debuginfo. The tracing libraries provide their API manuals and HTML documentation through their respective -doc packages.

Mozilla root-certificate data is installed by org.mozilla.ca-certificates at /usr/share/ca-certificates/mozilla.crt. The package supplies server-authentication roots from the tracked Firefox release data; it does not install machine trust policy by itself. A flat PEM store cannot express Mozilla's per-root distrust dates, so loading it alone does not reproduce every Firefox trust restriction.

The eudev family follows the same rule: io.github.eudev-project.eudev contains the daemon, tools, rules and Peios service integration; io.github.eudev-project.eudev-libudev contains the runtime library; and io.github.eudev-project.eudev-devel contains the public header, linker name and pkg-config metadata. Installing libudev for an application therefore does not pull in the system device manager. The device-manager package uses peiosutils for the core applets in its coldplug hook; it does not install the GNU Coreutils bootstrap package into the running system.

FIGlet is installed as org.figlet.figlet. Its commands, font catalogue and manuals form one small runtime package; detached debugging symbols and sources use the conventional -debuginfo and -debugsource suffixes.

The file-identification command is com.darwinsys.file. Its libmagic ABI, architecture-independent format database and development interface are split as com.darwinsys.file-libmagic, com.darwinsys.file-magic and com.darwinsys.file-devel, so library consumers do not acquire the command.

GNU find and xargs are installed as org.gnu.findutils; their manuals are in org.gnu.findutils-common. The locate and updatedb tools are omitted until Peios has a service design for maintaining their global index. Because Peipkg normalizes POSIX ownership and mode metadata, find predicates such as -user, -group and -perm inspect that compatibility metadata; they do not query KACS policy and must not be used as authorization checks.

GNU Bison's parser generator, yacc frontend and parser skeletons are in org.gnu.bison. Install org.gnu.bison-devel when a project uses the traditional liby.a convenience library or the bison-i18n.m4 Autoconf macro. Generated-parser message catalogs are independently available through org.gnu.bison-runtime.

GNU Binutils' assembler, linker and object tools are in org.gnu.binutils; headers, linker names and static libraries are in org.gnu.binutils-devel. The optional profiler is org.gnu.binutils-gprofng, including its command manuals. Its library's linker name and static archive are separately available in org.gnu.binutils-gprofng-devel, which also installs the matching profiler runtime. Ordinary compiler builds do not need the profiler.

Binutils supports Zstandard-compressed DWARF debug sections through objcopy --compress-debug-sections=zstd and ld --compress-debug-sections=zstd. The runtime package brings in com.facebook.zstd-libs; applications do not need the development package merely to use these commands. objcopy --decompress-debug-sections restores uncompressed debug sections when another consumer needs them.

The Flex scanner generator is io.github.westes.flex; its flex++ frontend is included with the command. The independently usable libfl runtime is io.github.westes.flex-libs, while io.github.westes.flex-devel adds the C++ header, static archive and linker name without folding those development files into either runtime package.

GNU Awk is installed as org.gnu.gawk and provides both the gawk and awk command names. Loadable extensions and the password/group lookup helpers stay with the interpreter, reusable Awk libraries and manuals are in org.gnu.gawk-common, and org.gnu.gawk-devel provides gawkapi.h for building additional extensions. Arbitrary-precision MPFR arithmetic is enabled. Persistent arrays are disabled because their process-wide ASLR manipulation is outside the Peios process contract. The interactive debugger (gawk -D) has GNU Readline line editing and command history.

The GNU debugger is installed as org.gnu.gdb, with the gcore, gstack and gdb-add-index helpers, Python scripting, the text user interface (gdb -tui), Readline line editing and iconv character-set conversion. It reads compressed debug sections (zlib, zstd and LZMA MiniDebugInfo) and finds a program's symbols in the matching -debuginfo package by build ID under /usr/lib/debug. The remote debugging stub is the separate org.gnu.gdb-gdbserver, and org.gnu.gdb-devel adds the JIT reader interface header. Guile scripting, CTF trace files, Intel Processor Trace, debuginfod lookups and source highlighting are not included.

The Expat XML parser library is installed as io.github.libexpat.expat; its headers, pkg-config and CMake files are in io.github.libexpat.expat-devel, the static library in io.github.libexpat.expat-static, and the xmlwf well-formedness checker in io.github.libexpat.expat-tools.

DWZ, the DWARF duplicate-removal tool, is installed as org.sourceware.dwz. Catalogue packages are built with it, so their -debuginfo payloads are already deduplicated per file. It can also optimise zstd-compressed DWARF while retaining Zstandard compression. Debugedit's tools are org.sourceware.debugedit; its RPM-style find-debuginfo driver is the separate org.sourceware.debugedit-find-debuginfo, which brings in GDB and DWZ.

util-linux's libblkid and getopt are joined by three command packages: org.kernel.util-linux-schedutils (taskset, chrt, ionice and renice, for CPU affinity and scheduling), org.kernel.util-linux-script (script, scriptreplay and scriptlive, for recording terminal sessions) and org.kernel.util-linux-extra (flock, setsid, rev, col, colcrt and colrm). util-linux commands that duplicate peiosutils, or that manage logons, storage, capabilities or namespaces, are not included.

Tcl 8.6 is installed as org.tcl-lang.tcl: tclsh, libtcl and the script library. Its headers (including the private ones under /usr/include/tcl-private), stubs library, tclConfig.sh and C API manuals are in org.tcl-lang.tcl-devel. Expect is org.tcl-lang.expect, providing the expect interpreter and package require Expect, with org.tcl-lang.expect-devel for embedding; Expect's example scripts (autoexpect, unbuffer, mkpasswd and the rest) are not shipped. The DejaGnu test harness, runtest, is org.gnu.dejagnu, and its dejagnu.h unit-test header is org.gnu.dejagnu-devel.

Three C and C++ unit-testing frameworks are available for building and running test suites:

  • GoogleTest and GoogleMock are com.google.googletest, with headers, the CMake package (find_package(GTest)) and pkg-config modules in com.google.googletest-devel and static libraries in com.google.googletest-static. The shared libraries' sonames carry the full release version, so test binaries rebuild with each GoogleTest update.
  • CUnit (the maintained CUnity continuation of SourceForge CUnit) is com.gitlab.cunity.cunit, which holds the XML report stylesheets; headers, the pkg-config module and the CMake package are in com.gitlab.cunity.cunit-devel. CUnit builds only a static library, com.gitlab.cunity.cunit-static, which the development package brings in.
  • Check is io.github.libcheck.check, with headers, the pkg-config module and the AM_PATH_CHECK Autoconf macro in io.github.libcheck.check-devel and the static library in io.github.libcheck.check-static. checkmk, which generates Check test programs from compact descriptions, is the separate io.github.libcheck.check-checkmk.

GNU Gettext's catalog commands are installed as org.gnu.gettext, with manuals, extraction rules, project templates and the commands' translated messages in org.gnu.gettext-common. Project maintainers install org.gnu.gettext-devel for autopoint, gettextize, the Autoconf macros and public headers. The independently usable libasprintf, libgettextpo and libtextstyle ABIs are separate runtime packages. Glibc supplies Peios's libintl; Gettext therefore does not install a competing implementation, while its commands and shell integration retain full native-language support. Java, C#, Emacs and terminal-styling integrations are not included. The optional po-fetch and AI-assisted spit helpers are also omitted until their complete runtime dependencies are available as Peios packages.

GNU gperf is installed as org.gnu.gperf. Its generated C and C++ source has no gperf runtime dependency, so no development or runtime-library subpackage is needed; debugging symbols and their matching source are available through the conventional org.gnu.gperf-debuginfo and org.gnu.gperf-debugsource packages.

GNU GMP's C runtime is org.gnu.gmp; the C++ wrapper is the independently installable org.gnu.gmp-c++, so C-only consumers do not acquire libstdc++. Headers and linker names are in org.gnu.gmp-devel, while static archives are in org.gnu.gmp-static. Builds use GMP's generic x86-64 runtime dispatch rather than instructions selected from the package builder's CPU, so the published libraries remain portable across Peios x86-64 systems.

GNU Grep is installed as org.gnu.grep and provides the grep, egrep and fgrep command names. Its manuals and translated messages are in org.gnu.grep-common. Basic, extended, fixed-string and Perl-compatible (grep -P) regular expressions are all supported; grep -P uses the PCRE2 library from org.pcre.pcre2, which is installed alongside Grep.

GNU gzip is installed as org.gnu.gzip, containing gzip, gunzip, zcat and uncompress; its manuals are in org.gnu.gzip-common. The optional org.gnu.gzip-utils package adds the non-interactive shell helpers such as zgrep, zdiff, zforce, znew and gzexe together with their declared command dependencies. zless and zmore are omitted until the catalogue has a pager provider, so installing the utilities never leaves unusable commands.

The Integer Set Library runtime is installed as io.sourceforge.libisl.isl. Headers, the linker name and pkg-config metadata are in io.sourceforge.libisl.isl-devel; the static archive and its static GMP closure are in io.sourceforge.libisl.isl-static. Published builds use ISL's portable mode rather than selecting instructions from the package builder's CPU.

Kernel module administration commands are installed as org.kernel.kmod; the independently usable libkmod runtime is org.kernel.kmod-libs. Headers and pkg-config metadata are in org.kernel.kmod-devel, with the static archive in org.kernel.kmod-static. The tools support gzip-, xz- and zstd-compressed modules and PKCS#7 signature reporting. depmod is a machine-facing command at /libexec/depmod; package transactions invoke it when a kernel-module payload changes.

The Linux capabilities runtimes libcap and libpsx are installed as org.kernel.libcap. Capability inspection and file-capability commands are in org.kernel.libcap-tools, development interfaces in org.kernel.libcap-devel, and both static archives in org.kernel.libcap-static. Go and PAM integrations are not included in this package family.

Libconfig's C runtime is installed as io.github.hyperrealm.libconfig; its self-contained C++ runtime is io.github.hyperrealm.libconfig-c++. Headers, linker names, pkg-config and CMake metadata for both interfaces are in io.github.hyperrealm.libconfig-devel, while the static archives are in io.github.hyperrealm.libconfig-static.

The libnl protocol-library family is installed as io.github.thom311.libnl. Its command suite, CLI support library, dynamically loaded traffic-control modules and lookup databases are in io.github.thom311.libnl-tools. Install io.github.thom311.libnl-devel for the complete public header and pkg-config surface, and io.github.thom311.libnl-static for the seven static libraries.

The hardware performance-event discovery and encoding runtime is net.sourceforge.perfmon2.libpfm. Its public headers, linker name and API/PMU manuals are in net.sourceforge.perfmon2.libpfm-devel; the static encoder is in net.sourceforge.perfmon2.libpfm-static. Python bindings are not included.

GNU Libtool's command-line tools, macros and support files are installed as org.gnu.libtool. The configured libtool command records the target compiler and ABI paths, so this package is architecture-specific even though its commands are shell scripts. The independently usable libltdl runtime is org.gnu.libtool-ltdl; its headers and linker metadata are in org.gnu.libtool-ltdl-devel, with the static archive in org.gnu.libtool-ltdl-static.

GNU M4 is installed as org.gnu.m4. The package includes the command, localised messages and manual; matching source, debuginfo and debugsource packages are published alongside it.

GNU Make is installed as org.gnu.make. Install org.gnu.make-devel to compile loadable modules against its gnumake.h interface. Guile integration is not included. Recipes use Peios' /usr/bin/sh system-shell path by default.

Meson is installed as com.mesonbuild.meson. The package includes the meson command and manual, and declares Ninja as a runtime dependency because Meson's normal build workflow invokes it. Meson's Python implementation is private to the application rather than exposed as a system-wide Python library.

GNU MPFR's shared runtime is installed as org.gnu.mpfr. Its public headers, linker name and pkg-config metadata are in org.gnu.mpfr-devel; the static archive and its static GMP closure are in org.gnu.mpfr-static. The library is built with thread-safe storage and publishes matching source, debuginfo and debugsource packages.

ncurses terminal-information utilities are installed as org.gnu.ncurses. The wide-character ABI 6 libraries are in org.gnu.ncurses-libs, while the terminal database and tabset files are in org.gnu.ncurses-terminfo. Headers, linker names, pkg-config metadata and API manuals are in org.gnu.ncurses-devel; static libraries are in org.gnu.ncurses-static. Narrow-character, C++ and Ada compatibility surfaces are not included.

The Ninja build executor is installed as org.ninja-build.ninja. Its Bash and Zsh completions, Vim syntax file, README, source manual and ninja(1) command reference are included with the command. The command reference is generated from the packaged executable's help output. Ninja uses the system shell to execute build rules, so the package depends on org.git.kernel.dash; matching source, debuginfo and debugsource packages are published alongside it.

IANA timezone data is installed as org.iana.tzdata under /usr/share/zoneinfo. The build uses the backward-compatible fat TZif format so older timezone compilers preserve final transitions that change daylight saving status or an abbreviation without changing the UTC offset.

The numactl command suite is installed as io.github.numactl.numactl. The shared libnuma runtime is io.github.numactl.libnuma; its headers, linker name, pkg-config metadata and API manuals are in io.github.numactl.libnuma-devel, while the static archive and its static libatomic closure are in io.github.numactl.libnuma-static. Matching source, debuginfo and debugsource packages are published alongside the family.

OpenSSL's general cryptography runtime, providers, engines and vendor configuration are installed as org.openssl.libcrypto; the TLS runtime is org.openssl.libssl. The openssl command and its user manuals are in org.openssl.openssl, while certificate-management Perl utilities are in the architecture-independent org.openssl.openssl-perl package. Headers, linker names, pkg-config and CMake metadata, and API manuals are in org.openssl.openssl-devel; static libraries are in org.openssl.openssl-static. Package-owned configuration is stored beneath /usr/etc/ssl and appears at OpenSSL's compiled /etc/ssl lookup path through Peios's merged configuration view. Matching source, debuginfo and debugsource packages are published alongside the family.

The kernel DWARF and BTF tool suite is installed as org.kernel.pahole. It includes pahole and the accompanying dwarves inspection tools, shell utilities and runtime data. The public libdwarves ABI is split into org.kernel.libdwarves, with headers and linker names in org.kernel.libdwarves-devel, so library consumers do not acquire the command suite. Release builds use upstream's signed tarballs because those contain the embedded libbpf sources needed for a complete build; matching source, debuginfo and debugsource packages are published alongside the family.

The ELF metadata editor is installed as org.nixos.patchelf. It includes the patchelf command, manual and Zsh completion, with matching source, debuginfo and debugsource packages. The command can inspect and change an ELF object's interpreter, run path, dynamic dependencies, SONAME and executable-stack state. Upstream release archives are not maintainer-signed, so newly discovered releases use an explicit HTTPS trust-on-first-use exception and become immutable once recorded in Pekit's SHA-256 lock.

PCI inspection and configuration tools are installed as cz.ucw.pciutils. It includes lspci, setpci, pcilmr, update-pciids and their manuals. The shared libpci ABI and bundled PCI ID database are in cz.ucw.libpci; headers, linker input, pkg-config metadata and the API manual are in cz.ucw.libpci-devel, while the static archive is in cz.ucw.libpci-static. Builds enable compressed ID files, explicit DNS lookups, kernel-module lookup through libkmod and udev HWDB fallback. Matching source, debuginfo and debugsource packages are published alongside the family, using release archives authenticated by maintainer Martin Mares's OpenPGP key.

Standard network-name databases are installed as org.debian.netbase. The package places the protocol, service and Ethernet-type registries in /usr/etc; Peios's merged configuration view exposes those vendor defaults as /etc/protocols, /etc/services and /etc/ethertypes for libc and networking tools. The RPC registry remains part of glibc at /usr/etc/rpc. Locally managed configuration can therefore override these files without modifying package payloads. Perl's shared runtime depends on netbase because its Socket, IO and Net::Ping modules perform named protocol and service lookups.

Perl is installed as org.perl.perl, which also provides the perl virtual capability used by existing build dependencies. Architecture-independent core modules are in org.perl.perl-modules; the shared embedding library and XS modules are in org.perl.libperl; and public CORE headers and linker names are in org.perl.perl-devel. Language, utility and module manuals are split into org.perl.perl-doc. The threaded interpreter uses stable major.minor module paths so a patch update does not abandon locally installed modules, and links the catalogue's zlib and bzip2 rather than bundled copies. Crypt, GDBM and DB extensions remain disabled until their libraries have production packages. Matching source, debuginfo and debugsource packages are published alongside the family. CPAN currently offers Sigstore bundles rather than a detached OpenPGP signature Pekit can verify, so discovered releases become immutable through their Pekit SHA-256 lock after HTTPS retrieval until Sigstore verification is available.

The system pkg-config implementation is org.pkgconf.pkgconf. It provides both the pkgconf and pkg-config command names and the bomtool, spdxtool and pccritic metadata utilities. Install org.pkgconf.pkgconf-devel when regenerating Autoconf builds that need the pkg.m4 macros. The independently usable shared library is org.pkgconf.libpkgconf; public headers, linker name and pkg-config metadata are in org.pkgconf.libpkgconf-devel, with the static archive in org.pkgconf.libpkgconf-static. Matching source, debuginfo and debugsource packages are published alongside the family.

GNU Readline's line-editing and history libraries are installed as org.gnu.readline, linked against ncurses' libtinfo. Headers, linker names, pkg-config metadata and the readline(3) and history(3) manuals are in org.gnu.readline-devel, with static archives in org.gnu.readline-static. Interactive bc, the gawk debugger and Python's readline module (which serves input() and the basic REPL selected by PYTHON_BASIC_REPL) use it, giving Emacs- or vi-style editing, history recall and ~/.inputrc key bindings. Bash builds its own bundled copy of Readline and does not depend on this package.

Rsync is installed as org.samba.rsync, which also provides and replaces the earlier rsync package name. The main package supports IPv6, iconv, generic extended attributes, OpenSSL-accelerated checksums, xxHash, zstd and rolling checksum SIMD. POSIX ACL support is omitted because Peios uses KACS DACLs. The TLS transport wrapper is in org.samba.rsync-ssl, and the Python-based restricted SSH-command wrapper is in org.samba.rrsync, so ordinary local transfers do not acquire either interpreter. The command can be invoked in daemon mode manually, but Peios does not currently install an rsync peinit service. Matching source, debuginfo and debugsource packages are published alongside the family.

For now, commands require the complete canonical name:

peipkg install com.amd.amd-ucode

References to concrete packages in package metadata likewise record complete canonical names; virtual capability names remain unqualified. Unqualified-name resolution may be added in future when the final component identifies exactly one package, but it is not part of the current command contract.

Peios Learn — documentation for the Peios project.

Built with Trail.