# peipkg

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

---

# 1.1 Overview

_Peios / Advanced Peios / peipkg / Introduction_

> What peipkg is — the program that fetches, verifies and installs software — what is unusual about it, and the shape of an operation.

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/bin/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 / Advanced Peios / peipkg / Introduction_

> The scope of this manual — the package manager, the producer toolchain and the image composer — and what is documented elsewhere.

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 / Advanced Peios / peipkg / Introduction_

> Terms this manual borrows unchanged from PSPU §5, and the ones it adds for itself.

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 / Advanced Peios / peipkg / Introduction_

> Which format and protocol version peipkg implements, the architectures it supports, and how it interoperates.

## 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 / Advanced Peios / peipkg / The Tools_

> The package manager itself — its verbs, the flags that change what a transaction may do, and how it exits.

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

## 2.1.1 Verbs

| Verb | Effect |
|---|---|
| `install` | Add packages, resolving and installing their dependency closure |
| `upgrade` | Move installed packages to newer versions; with no name, every installed package |
| `downgrade` | Move a package to an older version, requiring explicit authorisation |
| `uninstall` | Remove packages, cascading to or refusing on dependents |
| `undo` | Revert the effect of a previous transaction |
| `claim` | Inspect, grant, or revoke the holder of a role |
| `repo` | Add, list, remove, and refresh repositories |
| `query`, `list`, `info`, `owns` | Read the package database |
| `verify` | Re-hash installed files against what was recorded at install |
| `recover` | Resolve an interrupted transaction |
| `clean` | Garbage-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

| Flag | Effect |
|---|---|
| `--yes` | Confirm the routine "apply this plan?" prompt |
| `--cascade` | On uninstall, remove dependents rather than refusing |
| `--allow-stale` | Proceed despite a repository's trust state exceeding its maximum age |
| `--claim`, `--claim-all`, `--no-claim` | Change which roles an install claims (§9.4) |
| `--dangerously-bypass-path-restrictions` | Permit 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).

## 2.1.3 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 / Advanced Peios / peipkg / The Tools_

> The producer toolchain that turns a source tree into package files, and the three facts about it that matter here.

`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 a key named by
the recipe's signing configuration, and the signature entry is the last
thing written into the archive before compression.

---

# 2.3 peipkg-repo

_Peios / Advanced Peios / peipkg / The Tools_

> The tool that serves a repository — its verbs, what publishing verifies, the invariants it defends, and what it does not do.

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

## 2.3.1 Verbs

| Verb | Effect |
|---|---|
| `init` | Establish a new repository tree, with a descriptor and empty indexes |
| `publish` | Ingest one or more package files, derive both indexes, and sign everything |
| `verify` | Check 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.

`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 / Advanced Peios / peipkg / The Tools_

> Assembling an image without installing anything onto the building machine, sharing peipkg's resolver and archive reader.

`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 and hash.

**Build** reads the lock, fetches each package, checks its bytes against
the hash the lock recorded, 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 / Advanced Peios / peipkg / The Tools_

> peipkg's entire persistent state is one transactional store — why that choice is load-bearing, what it holds, and how it is protected.

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

| Content | Purpose |
|---|---|
| Installed packages | Name, version, architecture, originating repository, install time, and the stored manifest |
| Owned files | One row per path a package owns, with its type and the hash recorded at install |
| Repositories | Base URL, trust keys with their statuses, priority, signature policy, and the recorded freshness floor |
| Role holders | Which package holds each role |
| Claim links | Which links have been materialised, and for which role and slot |
| Transactions | The journal: pending and completed transactions, their operations, and the backup map |
| Machine metadata | The 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 / Advanced Peios / peipkg / The Tools_

> The four kinds of state peipkg keeps across three places — the database, repository configuration, the index cache, and staged files.

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.

---

# 3.1 Configuration

_Peios / Advanced Peios / peipkg / Repositories_

> A repository is a file naming a base URL and the policy applied to it — the local handle and the transport it uses.

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

| Setting | Meaning |
|---|---|
| Base URL | Where the descriptor and everything it points at are served from |
| Trust anchors | Expected key fingerprints, supplied out of band |
| Signature policy | `required` or `optional` (PSPU §5.37) |
| Priority | A positive integer; lower is higher priority |
| Minimum index version | The out-of-band freshness floor applied at first add |
| Maximum trusted age | How long a repository's trust state stays usable without a refresh |
| Insecure transport | Whether a non-HTTPS base URL is permitted for this repository |

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. peipkg identifies a
repository internally by the local handle, and compares an index's
declared repository name against that handle — so a configuration whose
handle differs from the descriptor's name produces a repository that
adds successfully and is then skipped at every install, with a warning
rather than an error.

## 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.

`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.2 Adding a Repository

_Peios / Advanced Peios / peipkg / Repositories_

> The trust ceremony where an operator decides a key speaks for a URL — the two forms, bootstrapping the freshness floor, and fetching keys before verifying.

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 no key matching an anchor verifies the descriptor, the add is
refused. The error names the condition but not the fingerprints
involved, which is the diagnostic a transcription error most needs.

peipkg does not display the fetched fingerprint alongside the supplied
one, and does not prompt for confirmation before recording trust. A
mismatch is caught — an anchor that does not match cannot verify
anything — but the operator is not shown the two side by side.

## 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 writes the floor unconditionally, including for a
repository already configured. Because the configured form of the
command needs no arguments and reads as idempotent, re-running it is the
route by which a recorded floor is replaced by whatever the current
fetch returns.

## 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 / Advanced Peios / peipkg / Repositories_

> Every repository carries a numeric priority deciding candidate selection and tie-breaks — and what priority pointedly is not.

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 / Advanced Peios / peipkg / Repositories_

> Bringing a repository's trust state and cached index up to date — the sequence, the freshness gate, maximum trusted age, and what is not checked.

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 checks apply to the active index. The archive index is verified for
signature and identity but is not subjected to the freshness floor.

## 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 / Advanced Peios / peipkg / Repositories_

> Indexes change when a repository publishes but are read on every operation — the cache that closes the gap, and how it is re-verified.

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 produces a warning,
and resolution proceeds without that repository.

For a repository the system depends on, that means a package the
operator expected to come from it is instead resolved from wherever else
it is available, at a lower priority.

## 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 / Advanced Peios / peipkg / Repositories_

> Everything a repository serves is a static file over HTTP — the size caps, URL resolution, supported schemes and failure handling.

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.

| Artifact | Cap |
|---|---|
| Repository descriptor | 4 MiB |
| Detached signature | 4 KiB |
| Public key file | 64 KiB |
| Index | 64 MiB |
| Package file | the 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, and enabling it produces no warning of its own on subsequent
operations.

`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 / Advanced Peios / peipkg / Repositories_

> The two relations that let one repository act on another's packages, and the gates on both.

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 has no configured
priority to compare, and peipkg skips the comparison — and with it the
guard — rather than treating the unknown origin as maximally trusted.

The consequence is worth stating plainly: for packages left behind by a
removed repository, the two gates above do not fire.

---

# 4.1 Inputs and Outputs

_Peios / Advanced Peios / peipkg / Resolution_

> Turning "install this" into an ordered plan, entirely from index data before anything is fetched — inputs, outputs and determinism.

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 / Advanced Peios / peipkg / Resolution_

> When a candidate satisfies a dependency — direct names and provides, constraints, roles as goals, and the architecture qualifier.

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
when checking a plan for consistency. It does not apply it while
selecting a candidate for a `noarch` package's dependency, where the
architecture test is skipped entirely.

The visible consequence is a plan that could have been satisfied being
rejected instead: a foreign-architecture candidate wins selection, and
the consistency check then rejects the whole resolution rather than the
one candidate.

---

# 4.3 Candidate Selection

_Peios / Advanced Peios / peipkg / Resolution_

> The ordered rules peipkg applies when several packages satisfy one dependency, and what happens when the rules run out.

When several available packages satisfy one dependency or goal, peipkg
chooses between them by applying the following rules in order. The first
rule that distinguishes two candidates decides.

1. **An exact architecture match beats `noarch`.** A candidate whose
   architecture equals the system's primary architecture is preferred
   over a `noarch` candidate of the same name.

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. **A higher package revision is preferred** — already implied by rule
   4, retained for clarity.

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

## 4.3.1 When the rules run out

Rules 1 to 6 do not order two *different versions of the same package*
matched through an unversioned `provides`: they carry no role version to
compare at rule 4, and they agree on name and repository at rule 6. Such
a pair is a complete tie, and the candidate enumerated first wins.

The outcome is therefore an artefact of the order the index was read in
rather than a consequence of the rules — which means the same index
served in a different order can install a different version.

---

# 4.4 Failure Conditions

_Peios / Advanced Peios / peipkg / Resolution_

> Every condition that makes resolution produce no plan, each with a machine-readable reason and the package it names.

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.

| Condition | Meaning |
|---|---|
| **Unsatisfiable** | A package in the candidate plan has a dependency no available package satisfies |
| **Conflict** | Two packages in the proposed resulting set trigger a conflict against each other |
| **Architecture mismatch** | A package in the plan is built for neither the system's primary architecture nor `noarch` |
| **Version regression** | An operation would move a package backwards without authorisation |
| **Cycle** | A dependency cycle the resolver cannot break by ordering |
| **Too complex** | The 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 / Advanced Peios / peipkg / Resolution_

> An optional dependency never enters a plan automatically, and is not carried into the resolver at all.

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 / Advanced Peios / peipkg / Resolution_

> Removing a package others depend on — blocking relations, cascading, and the system-critical packages that refuse either.

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 / Advanced Peios / peipkg / Resolution_

> A dependency is satisfied within one root — the two resolution modes, top-level placement, and cascading across roots.

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. `upgrade`, `downgrade`, `uninstall`, and
`undo` resolve single-root.

The consequence is that a dependency declaring a root resolves into that
root when the depending package is first installed, and is evaluated
against — and if missing, installed into — the depending package's root
on any later upgrade or removal.

## 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

Upgrading a package that is installed in several roots produces one
transaction per root. Those transactions are applied in sequence and
continue past a failure: a root whose upgrade fails is reported, and the
remaining roots are still attempted.

---

# 5.1 Preconditions

_Peios / Advanced Peios / peipkg / Installation_

> What must already be true before an install begins — the fetched file, the repository's trust state, the database and the plan.

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.

Within a single root, the guarantee holds: every package is provided and
verified before the first is materialised.

Across roots it does not. A cross-root transaction prepares and applies
each root in sequence, so one root's payload is on disk in its final
location before the next root's packages have been fetched or verified.

---

# 5.2 Validation

_Peios / Advanced Peios / peipkg / Installation_

> The package is decompressed, parsed and checked in full before anything is staged — including destinations, and what is not re-checked.

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 / Advanced Peios / peipkg / Installation_

> Computing what the install will do — which files and directories are created, which side effects fire, collisions and disk space.

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 / Advanced Peios / peipkg / Installation_

> Payload entries processed in archive order into staged paths, with nothing at a final location yet — modes, descriptors and hashing.

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.

Manifest-declared overrides are carried in the manifest and validated
for base64 decodability, decoded length, and sort order. They are not
checked against the payload — an override naming a path the package does
not ship, or naming a symlink, or decoding to bytes that are not a
security descriptor, is accepted — and they are not applied. No
descriptor peipkg supplies is ever an override.

The operator-facing policy of PSPU §5.20 — surfacing each override,
diffing it against inheritance, requiring confirmation for a non-official
repository — has nothing to act on as a result.

## 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 / Advanced Peios / peipkg / Installation_

> How an install path is computed and used, what the final component is protected against, and what is not protected.

An install path is computed by joining the payload-relative path to the
installation root, and then used as an ordinary path.

## 5.5.1 What is protected

The **final component** of every operation is symlink-safe. A staged
file is created with exclusive-create semantics, so it cannot land on an
existing file. A pre-existing entry at a destination is detected without
following it, and is displaced by a rename, which does not follow a
symlink at either end. A symlink already sitting at an install path is
therefore renamed aside rather than written through.

## 5.5.2 What is not

**Ancestor components are resolved by the kernel afresh, following
symlinks, on every call.** peipkg holds no directory descriptor across
an operation and re-walks each path string at each step.

Two consequences follow.

**A symlink ancestor redirects a write, with no race involved.** The
format's rules permit a package to ship a symlink whose target resolves
into a permitted destination, and permit a *different* package to ship a
file whose path descends through that symlink's location — each entry
validates in isolation. When the second package installs, the ancestor
symlink is followed, and the file lands where the symlink points rather
than where the archive said. The path recorded in the database is the
archive's path, so the collision constraint of §5.3 compares the wrong
name and does not fire.

**The window between check and commit is the whole transaction.** The
check for a pre-existing entry runs while the transaction's intent is
being journalled; the rename that acts on it runs in the apply phase,
after every package in the transaction has been downloaded, decompressed
and staged. Nothing is re-validated at commit, and no descriptor pins
the directory in between.

> [!NOTE]
> The format's symlink-target rules are the first layer of the defence
> described in PSPU §5.26, and component-wise resolution against a
> pinned parent descriptor is the second. Only the first is in place.
> The second is what PSPU §5.26 requires of a conforming consumer, and
> what closes the two cases above.

---

# 5.6 Registration

_Peios / Advanced Peios / peipkg / Installation_

> Recording what the install will mean — why the manifest is stored, and how file ownership is written down.

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 / Advanced Peios / peipkg / Installation_

> A path that already exists without belonging to any package — what peipkg does about it, and what it is meant to do.

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 What peipkg does

peipkg checks whether something exists at the install path. If it does,
the existing entry is renamed aside as a backup and the staged file is
renamed into place. If it does not, the staged file is renamed in.

The check is on existence alone. Ownership is not consulted, the
existing content is not compared against what is about to be installed,
and the operator is not asked.

The backup is discarded when the transaction commits, along with every
other backup (§8.2), so the displaced content does not survive the
operation.

## 5.7.2 The intended behaviour

PSPU §5 leaves the handling of an unowned pre-existing file to the
consumer, but the shape peipkg is designed for is three-way:

- **Adopt** it when its content is byte-identical to what would be
  installed. Recording the path as owned without rewriting it is safe,
  because the disk already holds exactly the intended bytes.
- **Fail** otherwise, naming the file, rather than overwriting something
  nobody claimed.
- **Displace** it when the operator explicitly authorises the overwrite,
  keeping the backup rather than destroying it.

None of the three is implemented. What happens today is the middle case
without the failure: overwrite, then discard the evidence.

---

# 6.1 The File Diff

_Peios / Advanced Peios / peipkg / Upgrade and Removal_

> An upgrade is one atomic operation combining an install and a removal — the four file categories, and their ordering at commit.

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.

| Category | Meaning | At commit |
|---|---|---|
| **Added** | In the new set, not the old | Staged and renamed in |
| **Replaced** | In both, with different content | Old renamed aside, staged renamed in |
| **Untouched** | In both, with identical content hash | Left alone |
| **Removed** | In the old set, not the new | Renamed aside |

peipkg does not compute the untouched category. Every payload entry of
the new version is staged and renamed into place, whether or not its
content differs from what is already there.

The effects are an upgrade that rewrites and backs up its whole payload
rather than the changed part of it, inode and timestamp churn on files
that did not change, and the configuration-file consequence described in
§6.2.

## 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.

Directories left empty by an upgrade are not removed.

---

# 6.2 Configuration Files

_Peios / Advanced Peios / peipkg / Upgrade and Removal_

> Configuration under /usr/etc is seed configuration, not effective configuration — modified detection, what is recorded, and why.

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 / Advanced Peios / peipkg / Upgrade and Removal_

> The preconditions an upgrade adds to an install's, the steps it runs, and what happens to the upgraded package's dependents.

## 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 / Advanced Peios / peipkg / Upgrade and Removal_

> Supersession after a rename — the guard on a replaces-triggered upgrade, and how it interacts with 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 / Advanced Peios / peipkg / Upgrade and Removal_

> A downgrade is an upgrade to an older version with one extra precondition — plus undo, and what a version revert does not revert.

## 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 / Advanced Peios / peipkg / Upgrade and Removal_

> The preconditions and steps of a removal, what happens to directories, and how overlapping ownership is handled.

## 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. **Prepare** the removal.
3. **Remove**: rename each path aside as a backup rather than deleting
   it, so that the uninstall can be rolled back.
4. **Schedule side effects** implied by what was removed.
5. **Deregister**: delete the package's record, withdraw any role it
   held, and reconcile any role whose claim paths it declared (§9.7).

Backups are discarded when the transaction commits.

## 6.6.3 Directories

Directory entries are skipped. A package's directories are left in
place, and its removal leaves the skeleton behind.

Because deleting the package's rows removes its ownership of those
directories, they become owned by nothing, and no later operation
reclaims them.

## 6.6.4 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. peipkg does not check for it during
removal, and does not surface it as a database-integrity warning.

---

# 6.7 Modified Files

_Peios / Advanced Peios / peipkg / Upgrade and Removal_

> A file whose content no longer matches the hash recorded at install — where the check runs, and what checking costs.

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 two 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 not. Every owned path is renamed aside regardless of
whether its content matches what was installed, and the operator is not
told that something they customised is being removed.

> [!NOTE]
> A modified file at removal time is either a customisation that removal
> destroys, or an unauthorised modification of a system file. Both are
> worth surfacing, and both currently pass silently.

## 6.7.2 The cost of checking

Hashing every installed file at uninstall is expensive: a large package
on slow storage takes seconds. The workable shape is to restrict the
check to paths where customisation is expected — configuration, and
locations policy names — and skip it for binaries and libraries, with
the operator able to authorise removal, skip the file, or abort.

---

# 7.1 Atomicity

_Peios / Advanced Peios / peipkg / Transactions_

> A transaction is the atomic unit of package work, with one durability boundary that everything commits across.

A transaction is the atomic unit of package work. Every install,
upgrade, uninstall, grant, and revoke executes within one, even when it
contains a single operation.

A transaction is atomic in the sense that either all of its operations
succeed and become visible, or none of them visibly take effect. That
holds under two kinds of failure:

- **Logical failure** — a step fails, an error is reported, or a
  cancellation is requested.
- **System failure** — power loss, kernel panic, hardware fault, at any
  point.

An **uncommitted** transaction is invisible to anything else on the
system: the database does not show its operations, staged files have not
replaced their targets, and no side effect has run. A transaction that
completes its commit step is **committed**, and its operations are
visible to everything afterwards.

## 7.1.1 The single durability boundary

Atomicity rests on one fact: the package database is a transactional
store, and **the database's own commit is the transaction's durability
boundary**. No separate commit protocol is layered on top of it.

A crash before that commit leaves the journal's transaction pending, and
recovery rolls it back. A crash after it leaves the transaction
committed, and recovery has only cleanup to finish.

The transaction is never found partly committed, which is why recovery
never has to complete a half-finished commit.

---

# 7.2 Scope

_Peios / Advanced Peios / peipkg / Transactions_

> What may go into one transaction, how operations are ordered, the one-operation-per-package rule, and cross-root 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.

Each root's transaction is prepared and committed in sequence. That has
a consequence for verification (§5.1): one root's payload is in place
before the next root's packages have been fetched.

Recovery for a cross-root transaction is described in §7.8, and is the
one place where roll-forward exists.

---

# 7.3 The Lock

_Peios / Advanced Peios / peipkg / Transactions_

> At most one transaction per root at a time — how the exclusive lock is acquired, why staleness cannot happen, and what it does not cover.

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 / Advanced Peios / peipkg / Transactions_

> The five steps that take a transaction from uncommitted to committed, and what each one guarantees.

Commit transitions a transaction from uncommitted to committed, in five
steps.

## 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 the durability boundary.** It is atomic, so
the transaction is either fully committed or not committed at all.

## 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.

Because step 3 is a single atomic database commit, and because it
carries both the new state and the journal's closure, there is no
intermediate state to discover.

---

# 7.5 The Journal

_Peios / Advanced Peios / peipkg / Transactions_

> The journal is rows in the package database rather than a separate file — what it holds, its integrity, attribution and versioning.

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 kind | Content |
|---|---|
| Transaction | Identifier, state, schema version, and for a cross-root operation the shared cross-root identifier |
| Operation | One per package operation: kind, package, root |
| File | One per file operation: final path, staged path, backup path, action |
| Directory | One per directory the transaction created, so rollback can remove it |
| Commit payload | For 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 / Advanced Peios / peipkg / Transactions_

> Repeated side effects are deduplicated and invoked once per transaction, after every operation completes.

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 / Advanced Peios / peipkg / Transactions_

> Queries see only committed transactions under snapshot isolation, and the one place that boundary is softer.

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.

In-progress state is not visible outside peipkg's own process. Staged
files do not appear at their final install paths until the apply phase,
and journal rows describe a transaction that queries do not see.

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

Two 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.

---

# 7.8 Crash Recovery

_Peios / Advanced Peios / peipkg / Transactions_

> peipkg checks the journal before permitting new work — why it rolls back rather than forward, and the cross-root exception.

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.

Recovery of a cross-root operation therefore does roll forward. Each
root's transaction persists the state a completion would need, and a
root found pending after a sibling has committed is completed from that
record rather than reversed.

A root found pending with no persisted payload cannot be completed and
cannot safely be reversed, and recovery refuses it, leaving the
operation for an operator.

## 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 / Advanced Peios / peipkg / Rollback and Recovery_

> Rollback is possible at any point before the database commit — when it happens, the procedure, and the order it undoes things in.

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.

## 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 / Advanced Peios / peipkg / Rollback and Recovery_

> A displaced original is renamed aside within its own directory and never copied — the naming scheme and the retention rules.

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 / Advanced Peios / peipkg / Rollback and Recovery_

> A successful rollback leaves the system indistinguishable from before the transaction — and what happens when rollback itself fails.

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.

What happens is different. Rollback errors are discarded at every site
that triggers one, and the journal's transaction is then closed as
rolled back regardless.

The consequences are worth stating plainly. If a rollback fails partway:

- 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` will report the affected files as modified, because
their recorded hashes no longer match what is on disk. That is the only
signal.

---

# 8.4 Indeterminate State

_Peios / Advanced Peios / peipkg / Rollback and Recovery_

> A failed rollback or corrupt journal leaves a state peipkg cannot reason about — what exists today, and what an operator can do.

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

None of the five.

There is no indeterminate state as a concept: a transaction is pending
or it is not. Writes are not refused after a failure; reads carry no
warning; there is no forensic report; `peipkg recover` offers only
rollback, with no way to accept the current state and discard the
journal; and recovery runs automatically at the head of every operation,
with no prompt.

The last point is the sharpest. Automatic rollback of a *pending*
transaction is correct and is what §7.8 describes. But because nothing
distinguishes a cleanly pending transaction from an indeterminate one,
automatic resolution is the only behaviour available for both.

## 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` rolls back a pending transaction explicitly, and emits
an audit event where the automatic path does not.

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 / Advanced Peios / peipkg / Rollback and Recovery_

> Upgrading the package manager is not a special case — the binary is one file among the payload, with one wrinkle.

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 / Advanced Peios / peipkg / Rollback and Recovery_

> Reverting to an earlier version is an ordinary downgrade through the ordinary machinery — its procedure, constraints and scope.

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 / Advanced Peios / peipkg / Roles and Claims_

> A role is a virtual name several packages contend to own, with at most one holding it — the pieces, and why the links belong to peipkg.

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/bin/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/bin/registryd` pointing at
`/usr/sbin/loregd` is written as `../sbin/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 / Advanced Peios / peipkg / Roles and Claims_

> What makes a package an eligible provider of a role — what peipkg checks, and what it deliberately does not.

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 checked for structural sanity: absolute,
within the length limit, lexically clean, with a non-empty first
component.

## 9.2.2 What peipkg does not check

Neither a target nor a claim path is checked against the permitted
install destinations, and neither is subject to the payload path-syntax
constraints — normalisation form, control characters, backslashes,
component length.

A target is not checked against the declaring package's own payload
either. The producer-side library offers that check and pekit runs it,
but peipkg does not run it at install time, so a package built by
anything else can declare a target it does not ship.

The visible consequences, in order of severity: a claim path outside the
managed tree is materialised there, displacing whatever was at that path
into a backup that the commit then discards; a target naming a path the
package does not own produces a link pointing at whatever is there; and
a target naming nothing produces a dangling link.

---

# 9.3 Auto-Claim

_Peios / Advanced Peios / peipkg / Roles and Claims_

> Installing an eligible provider claims every unheld role it provides, and what happens when one transaction brings two providers.

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 / Advanced Peios / peipkg / Roles and Claims_

> The three flags that modify what an install claims, the contradictions between them, and naming a role the package does not provide.

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

| Flag | Effect |
|---|---|
| `--no-claim` | Claim nothing, including unheld roles |
| `--claim <roles>` | Claim each named role, even if another package holds it |
| `--claim-all` | Claim 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 / Advanced Peios / peipkg / Roles and Claims_

> Listing providers and granting a role by hand, addressed by role, with the grants that are refused.

`peipkg claim` inspects and changes holders independently of install.

| Form | Effect |
|---|---|
| `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> revoke` | Revoke 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 / Advanced Peios / peipkg / Roles and Claims_

> Recomputing a role's links from current state and applying the difference — when it runs, held but unmaterialised, collisions and repointing.

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.

The check reads the ownership table as it stands *before* the
transaction's own rows are written, which happens at commit. So a
transaction that installs both a package owning a path and a provider
claiming that same path sees no owner, queues both a payload operation
and a claim operation for one destination, and commits both — leaving
the database holding an ownership row and a claim-link row for one path.

A package that owns a **directory** at the claim path is waved through
rather than treated as a collision, and the directory is renamed aside
and replaced with the link.

The reverse direction is unguarded: nothing stops a package payload
installing over an existing claim link. The link is renamed aside, the
payload file takes the path, and the claim-link row survives — after
which reconciliation compares its record against its own desired set,
finds them equal, and never notices the link is gone.

## 9.6.4 Repointing

A holder swap repoints every one of the role's links, within a single
transaction.

Each repoint is performed as two renames: the old link is renamed aside
as a backup, then the new link is renamed into place. The path is absent
between the two.

> [!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. A single rename that replaces the old link
> outright closes the window; two renames leave it open for a full
> rename round-trip, on every link of the role.

---

# 9.7 Withdrawal

_Peios / Advanced Peios / peipkg / Roles and Claims_

> What happens to a role when its holder is uninstalled, why nothing is promoted automatically, and how it is ordered within the uninstall.

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 'altregd', 'thirdregd'
also provide it. Run 'peipkg claim registryd grant 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 / Advanced Peios / peipkg / Installation Roots_

> A self-contained tree with its own package database — the anchor, the naming grammar, and why nesting is structural.

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 / Advanced Peios / peipkg / Installation Roots_

> Named children as rows in the owning root's database, and how a root reference is resolved.

## 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 / Advanced Peios / peipkg / Installation Roots_

> A dependency is satisfied within a root — how routing works, which verbs route, satisfier identity, and cross-root garbage collection.

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.

```json
{ "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. `upgrade`, `downgrade`, `uninstall`, and
`undo` resolve single-root, where a dependency's `root` field is inert
and the dependency is evaluated in the depending package's root instead.

So a cross-root dependency is placed correctly when the depending
package is installed, and is evaluated in the wrong root on every later
operation.

## 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 / Advanced Peios / peipkg / Installation Roots_

> Building a fresh root deterministically without touching the host or running package code — the manifest, the lock, and the three phases.

`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:

| Key | Meaning |
|---|---|
| `arch` | The primary architecture, which becomes the composed database's recorded value |
| `source_date` | The timestamp everything is stamped with |
| `local_packages` | Globs 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, and root. It carries a digest of the
manifest, so that building against a stale lock is caught.

The digest covers the packages, their constraints, and their repository
pins. It does not cover root declarations or per-package root placement,
so a manifest edit that only moves a package between roots, or changes
where a root lives, produces the same digest — and a build without an
explicit update silently reproduces 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, checks its bytes against the hash the
lock recorded, validates its format, and cross-checks the archive's own
manifest against the lock entry. Every package is verified before any is
extracted.

What is not checked is the inline signature against a trust set. The
resolve phase performs the trust ceremony and then discards the trust
state with the temporary database it was built in, so the build phase
has hashes but no keys. The chain that remains is: the index was signed,
the lock records the index's hash, the bytes match the hash. A package
whose signing key has since been **revoked** is accepted by that chain
and refused by peipkg.

**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.

## 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.

## 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 registered but never
created. The registration points at a directory that does not exist, and
addressing that root on the composed system fails when peipkg tries to
open its database.

---

# 11.1 The Recognised Set

_Peios / Advanced Peios / peipkg / Side Effects_

> A package cannot ship install-time code; it declares one of three standard maintenance operations, and peipkg runs it.

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

| Identifier | Rebuilds |
|---|---|
| `ldconfig` | The shared library cache, `/etc/ld.so.cache`, and shared library symlinks |
| `depmod` | The kernel module dependency cache — `modules.dep` and its companions under `/usr/lib/modules/<release>/` |
| `man-db` | The 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 is rejected, and a
duplicate within the array is rejected.

## 11.1.1 When each is required

A package containing shared libraries declares `ldconfig`; one
containing none does not. 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. It does
not validate them against the payload: a package shipping shared
libraries with no `ldconfig` declaration packs, installs, and leaves the
library cache stale, and a package declaring `ldconfig` while shipping
no libraries invokes it for nothing.

The producer is where that check belongs — the payload map it would
examine is already walked to derive shared-library capabilities — and
neither the producer nor the consumer performs it.

## 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 / Advanced Peios / peipkg / Side Effects_

> Each side effect maps to one fixed command, and the three properties that keep a package from influencing how it runs.

Each side effect maps to one fixed command.

| Identifier | Invoked as |
|---|---|
| `ldconfig` | `/bin/ldconfig`, no arguments |
| `depmod` | `/bin/depmod -a` |
| `man-db` | `/bin/mandb -q` |

## 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 -a` acts on the *running* kernel, since no release is named.

A transaction installing modules for a kernel release other than the one
currently booted — which is the normal case during a kernel update, and
always the case during an image build — rebuilds the running kernel's
dependency cache and leaves the installed release's unbuilt, so those
modules are unloadable until something rebuilds it.

A package shipping modules for two releases gets one invocation, for
neither of them necessarily.

## 11.2.3 The root

The tools invoked are the **host's**, at the host's absolute paths, with
no root argument.

An operation against an alternate installation root therefore rebuilds
the host's caches rather than the target's — once per participating
root, in a cross-root transaction, and never for the root whose contents
changed.

`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 / Advanced Peios / peipkg / Side Effects_

> Side effects run at commit after every package is registered — what failure does, and what deliberately does not schedule one.

## 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
`ldconfig` 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 library cache is rebuilt after every
package's libraries 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 three 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 What does not schedule

An operation that only removes files schedules nothing, so removing the
last package that owned a shared library or a kernel module leaves the
corresponding cache naming something that no longer exists (§7.6).

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

---

# 12.1 The Recipe Family

_Peios / Advanced Peios / peipkg / Producing Packages_

> A recipe is a family of TOML documents rather than one file — strict parsing throughout, and how the layers merge.

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.

| File | Role |
|---|---|
| `pekit.toml` | The recipe: source acquisition, and the build, test, install, clean, and gen targets |
| `workspace.pekit.toml` | Workspace marker: member globs, distro-wide environment, derivation policy |
| `package.pekit.toml` | Shared package metadata — the base layer |
| `<selector>.package.pekit.toml` | One emitted package per file |
| `packages.pekit/` | An alternative directory holding the two above |
| `env.pekit.toml`, `<name>.env.pekit.toml` | A build entry's environment, wrapper, and dependency provider |
| `<name>.keyring.pekit.toml` | A secrets tree exported into the build environment |
| `pekit.lock` | Machine-written source pin |
| `<patches>/series` | The 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 / Advanced Peios / peipkg / Producing Packages_

> The recipe's own file — environment variables, wrapping, build targets and generation targets.

`pekit.toml` has ten top-level keys.

| Key | Type | Meaning |
|---|---|---|
| `out_dir` | string | Stage and output base, relative to the recipe root. Defaults to `out` |
| `env` | table | Environment for target commands |
| `wrap` | table | A command wrapper applied to every target command |
| `source` | table | Where the source comes from |
| `delegate` | bool or table | Borrow build targets, environment, wrapper, or package definitions from the fetched source tree |
| `build`, `test`, `install`, `clean` | tables | Target namespaces |
| `gen` | table | Generation targets, with their own verification |
| `source_package` | table | Whether 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.

| Key | Meaning |
|---|---|
| `command` | Required. A shell string, or a non-empty argv array |
| `needs` | Other targets that run first |
| `clear_out` | Whether to clear the target's stage directory first. Defaults to true |
| `dependencies` | Build-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.

| Key | Meaning |
|---|---|
| `command` | Required. Regenerates the artifact |
| `verify_command` | Checks that the committed artifact is up to date |
| `verify_on_build`, `verify_on_test` | Which targets in that namespace the verification gates |
| `dependencies`, `verify_dependencies` | As 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 / Advanced Peios / peipkg / Producing Packages_

> Where a build's inputs come from — version control, fetched artifacts, local trees and patches — with signature verification and the lock.

`[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

| Key | Meaning |
|---|---|
| `url` | Required |
| `ref` | Templated, defaulting to the version placeholder |
| `versions` | A constraint capping which upstream tags are eligible |
| `tag_regex` | A pattern filtering tags during version enumeration |

## 12.3.2 A fetched artifact

| Key | Meaning |
|---|---|
| `url` | Required, templated |
| `extract` | Whether to unpack the artifact |
| `root` | The subdirectory of the unpacked tree that is the source root |
| `versions` | A constraint cap |
| `file_regex` | A pattern applied to a directory listing to enumerate versions |
| `checksum` | A 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.

| Key | Meaning |
|---|---|
| `url` | Templated; defaults to the source URL with a signature suffix |
| `of` | `artifact` or `decompressed` — which bytes the signature covers |
| `key_files` | Required and non-empty: the pinned public keys |
| `fingerprints` | An allowlist of acceptable signing fingerprints |

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.

## 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 / Advanced Peios / peipkg / Producing Packages_

> Each emitted package described by its own file layered over a shared base — metadata, relationships, files, symlinks, multipack and publish.

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

| Key | Meaning |
|---|---|
| `format` | `tar` (the default) or `peipkg` |
| `clear_out` | Whether to clear the package's stage directory first |
| `builds` | Which build targets to stage; inferred from the file references when omitted |
| `package` | The metadata table |
| `files` | Source reference to destination |
| `symlinks` | Destination to target |
| `excludes` | Patterns removed from the matched set |
| `multipack` | Fan this one definition into several packages |
| `publish` | Where to publish the result |
| `dependencies`, `optional_dependencies`, `conflicts`, `provides`, `replaces` | The manifest relationships |
| `side_effects` | The declared maintenance operations |
| `sd_overrides` | Path to security descriptor |
| `claims` | Role 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 Relationships

Dependencies take either of two forms:

```toml
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](/peios/advanced-peios/peipkg/producing-packages/building-and-signing.md).

## 12.4.3 Files

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

| Reference | Means |
|---|---|
| `@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.4 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.5 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.6 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 / Advanced Peios / peipkg / Producing Packages_

> The two mechanisms that make the shipped manifest more than what the recipe wrote — placeholder substitution and derived capabilities.

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}}`, `{{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}}`; for a version carrying
an epoch or a tilde, the model does not parse it and the components
render empty.

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

```toml
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 / Advanced Peios / peipkg / Producing Packages_

> Fetching, patching, building into stage directories, packing the result, signing it, and recording the corresponding source.

## 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: the workspace layer,
the source layer, the recipe layer, the selected environment file, and
the keyring, in that order, wrapped by the recipe's wrapper if it
declares one.

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.

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.

| Payload | Declaration | Result |
|---|---|---|
| A `.ko` or `.ko.*` under `usr/lib/modules/` | no `depmod` | Error |
| No kernel module | `depmod` declared | Error |
| A file under `usr/share/man/` | no `man-db` | Warning |
| No man page | `man-db` declared | Warning |

`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 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.

---

# 12.7 Reproducibility

_Peios / Advanced Peios / peipkg / Producing Packages_

> The format's determinism rules are necessary but not sufficient — what the build has to control, and how to verify a package reproduces.

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 / Advanced Peios / peipkg / Security_

> peipkg holds no principal and no rights of its own — it runs as the calling operator, and every file operation is checked against their token.

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 / Advanced Peios / peipkg / Security_

> With no standing privileged identity there is nothing to confine and nothing to compromise — and what peipkg therefore does not do.

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 / Advanced Peios / peipkg / Security_

> Every install, upgrade, uninstall, refresh and recovery emits an event through KMES — what one carries, and what emission depends on.

Every install, upgrade, uninstall, refresh, and recovery emits an audit
event.

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 an event carries

| Field | Where it lives |
|---|---|
| Operation type | The event's type tag |
| The caller's identity | **The kernel-stamped header** |
| Target packages | The payload: name, version, architecture |
| Outcome, with a rejection reason | The payload |
| Transaction identifier | The payload |
| Timestamp, UTC | The payload |
| Source repository | Not carried |

Identity is not in the payload and is not peipkg's to write. The kernel
stamps the caller's effective token, its true token, and its process
identity onto every emission, where they cannot be forged or suppressed
by the emitting program. That is a stronger guarantee than a payload
field: peipkg could lie about a payload field, and cannot lie about a
header the kernel wrote.

The source repository is not recorded on an install or upgrade event,
although it is known. A plan drawing packages from several repositories
names none of them.

A committed cross-root operation emits its success event without a
transaction identifier, so it cannot be joined to the transaction ledger
or to the kernel's own record of the file operations it performed.

## 13.3.2 Event types

| Type | Emitted for |
|---|---|
| `peipkg.install` | A successful install |
| `peipkg.upgrade` | A successful upgrade — and a downgrade, and an undo |
| `peipkg.uninstall` | A successful uninstall |
| `peipkg.refresh` | A repository refresh |
| `peipkg.transaction-failed` | A transaction that was rolled back |
| `peipkg.recovery` | A recovery-mode resolution |
| `peipkg.authorisation` | An operator authorisation record |
| `peipkg.repo-add` | A repository add |
| `peipkg.repo-remove` | A repository remove |
| `peipkg.config-change` | A trust-policy or transport-flag change |
| `peipkg.claim` | A claim grant or revoke |

Downgrade and undo are deliberately recorded as upgrades, since the set
carries no downgrade type.

`peipkg.config-change` is declared and never emitted. A repository
re-added with a weakened signature policy or an enabled transport
allowance produces an event indistinguishable from a routine add, so
trust-policy history cannot be reconstructed from the stream.

A refresh in which some repositories succeeded and others failed emits
one event with a rejection outcome, an empty repository field, and a
count in its detail.

## 13.3.3 Successes and failures

A committed operation emits a success event; one that is rejected or
rolled back emits a separate failure event. Rejection reasons and error
text travel in the detail field.

An operator who declines at a prompt emits nothing: the transaction
never started.

An automatic recovery at the head of an ordinary operation emits
nothing. The same rollback performed deliberately through the recover
command does. `peipkg recover`'s own failure paths emit nothing either.

`peipkg-compose` emits nothing at all.

## 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 event.

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 / Advanced Peios / peipkg / Security_

> The points requiring a deliberate act specific to the elevated action, why --yes does not satisfy them, and what is left ungated.

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 action | Gate |
|---|---|
| Downgrade | A per-action prompt, audited |
| A low-trust provider filling a high-trust role | A per-action prompt, audited |
| A foreign `replaces` against a higher-priority package | A per-action prompt, audited |
| Proceeding on stale trust state | The `--allow-stale` flag, audited, no prompt |
| Installing unsigned content under an `optional` policy | Not gated, not audited |
| Enabling insecure transport | Not gated, not audited |
| Resolving an interrupted transaction | Not 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 The channel is not distinguished

An authorization prompt and the routine prompt read from the same input
stream and accept the same affirmative. The distinctness the model calls
for is a property of what is *displayed*, not of what is *accepted*.

A script piping affirmatives satisfies every elevated gate in a plan
along with the routine one. The property survives for a person at a
terminal and does not survive automation.

## 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 / Advanced Peios / peipkg / Security_

> A key fingerprint supplied out of band is the root of the whole repository trust model — where anchors come from, and what that means.

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 / Advanced Peios / peipkg / Security_

> The checks that depend on the local system clock, and what a wrong clock does to each of them.

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 / Advanced Peios / peipkg / Security_

> What the package manager explicitly does not defend against, and where each of those concerns is addressed instead.

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 / Advanced Peios / peipkg / Failure Modes_

> Power loss or a kill partway through — what the journal preserves, what happens next, and how a post-commit crash differs.

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

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

Recovery rolls forward instead. A root found pending after a sibling
committed is completed from the state its journal persisted, because a
committed sibling cannot be undone.

A pending root carrying no persisted state can be neither completed nor
safely reversed, and recovery refuses it — blocking further work in that
root until `peipkg recover` is run.

A pending cross-root transaction found by an ordinary single-root
operation is refused rather than recovered, for the same reason.

## 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 / Advanced Peios / peipkg / Failure Modes_

> When the rollback itself fails — what is left on disk, what peipkg does about it, and what it means for the next operation.

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

Discards the error, closes the journal's transaction as rolled back, and
returns.

## 14.2.3 What that means for the next operation

Recovery looks for a pending transaction and finds none, so it does not
retry. The history shows an authoritative-looking rolled-back record.
Nothing tells the operator the rollback did not complete.

## 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

Identify the affected paths, decide whether the old or the new content
is wanted, remove the leftover siblings, and reinstall the affected
packages so that the database and the filesystem agree again.

---

# 14.3 A File That Does Not Match

_Peios / Advanced Peios / peipkg / Failure Modes_

> A file whose hash differs from the one recorded at install — what that can mean, how to tell the cases apart, and reinstalling.

`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 / Advanced Peios / peipkg / Failure Modes_

> Fetch failure during a refresh or an install — why the previous trust state is retained and nothing falls back to unverified.

## 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 / Advanced Peios / peipkg / Failure Modes_

> peipkg does not check free space first — where in the operation the write fails decides how bad it is.

peipkg does not check free space before it starts. Exhaustion is
discovered when a write fails, 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 Planning ahead

A transaction's additional requirement is the staged new and changed
content, aggregated across every operation. Backups cost nothing.

Because a transaction can span several filesystems — installation roots
and destinations within a root can be separate mounts — the useful
figure is per filesystem rather than a single total.

Nothing computes either figure. The manifest's declared installed size
is available and is used to bound decompression, not to plan space.

---

# 14.6 A Broken Claim

_Peios / Advanced Peios / peipkg / Failure Modes_

> A claim path missing, dangling or pointing at the wrong thing — what each state means, and how to repair it.

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 that landed outside the managed tree, because claim paths
are not constrained to the permitted destinations, displaces whatever
was there. The displaced file went to a backup that the commit
discarded.

## 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 / Advanced Peios / peipkg_

> Every path peipkg reads or writes — per root, transient names, composition, side-effect tools and producer files.

## A.1 Per installation root

| Location | Content |
|---|---|
| `<root>/var/state/peipkg/db.sqlite` | The package database: installed packages, owned files, repositories, claims, named roots, and the transaction journal (§2.5) |
| `<root>/lcl/conf/peipkg/<name>.repo` | One 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

| Pattern | Meaning |
|---|---|
| `<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-new` | A 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

| Location | Content |
|---|---|
| `<manifest-stem>.lock.toml` | The pinned closure a composition resolves to |
| `<out>.peipkg-compose-tmp` | The tree under construction, renamed into place on success |
| `<out>/usr/share/licenses.json` | The license inventory the composer writes, owned by no package |

## A.4 Side-effect tools

| Identifier | Invoked |
|---|---|
| `ldconfig` | `/bin/ldconfig` |
| `depmod` | `/libexec/depmod -a` |
| `man-db` | `/bin/mandb -q` |

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

| File | Role |
|---|---|
| `pekit.toml` | The recipe |
| `workspace.pekit.toml` | The workspace marker |
| `package.pekit.toml`, `<selector>.package.pekit.toml` | Package definitions, layered |
| `packages.pekit/` | An alternative directory for the above |
| `env.pekit.toml`, `<name>.env.pekit.toml` | Build environments |
| `<name>.keyring.pekit.toml` | Secrets, including the package signing key |
| `pekit.lock` | The source pin |
| `<patches>/series` | The patch series |

---

# Appendix B Audit Events

_Peios / Advanced Peios / peipkg_

> Every audit event peipkg emits — the types, their payload fields, where events do not appear, and what emission depends on.

Every event described here is emitted into the kernel event subsystem
(§13.3). The caller's identity is stamped into the event header by the
kernel and is not part of the payload.

These types also appear in the
[Peios Events Index](/peios/using-peios/events/package-events/the-event-set.md), alongside
every other event the system emits.

## B.1 Types

| Type | Emitted for | Emitted today |
|---|---|---|
| `peipkg.install` | A successful install | yes |
| `peipkg.upgrade` | A successful upgrade, downgrade, or undo | yes |
| `peipkg.uninstall` | A successful uninstall | yes |
| `peipkg.refresh` | A repository refresh, successful or partially failed | yes |
| `peipkg.transaction-failed` | A rejected or rolled-back transaction | yes |
| `peipkg.recovery` | A recovery resolved through `peipkg recover` | yes |
| `peipkg.authorisation` | An operator authorisation record | yes |
| `peipkg.repo-add` | A repository add | yes |
| `peipkg.repo-remove` | A repository remove | yes |
| `peipkg.claim` | A claim grant or revoke | yes |
| `peipkg.config-change` | A trust-policy or transport-flag change | **no** |

## B.2 Payload fields

| Field | Content |
|---|---|
| `txn_id` | The transaction identifier |
| `outcome` | `success`, `rejection`, or `rollback` |
| `repo` | The repository, for repository operations |
| `detail` | The rejection reason, the operation count, or the authorised action |
| `timestamp` | RFC 3339, UTC |
| `packages` | Name, version, and architecture per package |

## B.3 Where events do not appear

- An install or upgrade event carries no source repository, although one
  is known.
- A committed cross-root operation's success event carries no
  transaction identifier.
- Automatic recovery at the head of an ordinary operation emits nothing.
- `peipkg recover`'s failure paths emit nothing.
- Declining at a prompt emits nothing.
- Enabling insecure transport, and installing unsigned content under an
  `optional` policy, emit no authorisation record.
- `peipkg-compose` emits 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.

On a kernel with no emit call, emission is a silent successful no-op.
