# Package management

---

# Package management

_Peios / Using Peios / Package management_

> peipkg installs, upgrades, removes, and queries software on a Peios system. This page covers packages, repositories, the security model, and the commands.

`peipkg` is the command that manages software on a running Peios system. It installs packages and the dependencies they need, upgrades them, removes them, and answers questions about what is installed and where it came from.

```
$ peipkg install nginx
$ peipkg upgrade
$ peipkg list
```

This page explains what a package is, where packages come from, and the design principle that peipkg holds no authority of its own. It then names every command and points you at the rest of the topic.

## What a package is

Peios distributes software as **`.peipkg` files**: signed, self-contained archives. Each one carries a **manifest** — a name, a version, a target architecture, the other packages it depends on, the packages it conflicts with — and a **payload**, the files that land on disk when it is installed.

A package is a **low-level primitive**. It is the unit peipkg installs and tracks. The curated, user-facing concepts that most operators think in — bundles of software, "the web-server role", applications — are built above packages and are out of peipkg's scope: peipkg installs `nginx` the package; it does not know what a "web server role" is. This topic is about the primitive.

What peipkg does coordinate at the package level is [claims](/peios/using-peios/package-management/claims.md): a claim is a single shared name that exactly one installed package may hold, so that two packages offering the same role — for example, `registryd` and `loregd` both providing the registry — do not silently collide.

Every installed package is tracked in a private database. peipkg records, for every package, which files it owns — the record that makes a clean removal, a correct upgrade, and the [`verify`](/peios/using-peios/package-management/inspecting-and-verifying.md) check possible.

## Where packages come from

Packages come from **repositories**: HTTP or HTTPS locations that serve a set of `.peipkg` files alongside signed indexes describing them. A Peios system is configured with one or more repositories, each one **anchored to a signing key the operator has chosen to trust**.

peipkg keeps a local, verified copy of each repository's metadata. `peipkg refresh` updates that copy; `peipkg install` and `peipkg upgrade` plan against it. The trust model — how a repository is anchored, how its signing keys rotate, and how unsigned repositories are handled — is covered in [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

A package can also be installed straight from a `.peipkg` file on disk, with no repository involved. That path trades the repository's trust guarantees for convenience; [Installing and removing packages](/peios/using-peios/package-management/installing-and-removing.md) covers what it keeps and what it gives up.

```mermaid
flowchart LR
    A["Repository<br/>signed packages + indexes"] -->|"refresh"| B["Cached metadata"]
    B -->|"install / upgrade"| C["peipkg<br/>resolve → verify → commit"]
    C -->|"file changes"| D["Your system"]
    C -->|"operation events"| E["Audit stream"]
```

## peipkg has no authority of its own

The key design point is that peipkg works differently from package managers on most systems.

peipkg is not a privileged daemon. It is not setuid. It has no service account, no special identity, no broker it asks to do privileged work. It is an ordinary program, and it runs as you — under your token, with your rights.

The question "may I install this?" is therefore not a question peipkg answers. It is the same question Peios asks of any attempt to write a file: it compares your token against the [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) on the directory being written. If the security descriptors on `/usr`, `/opt`, and the rest say your token may write there, the install succeeds. If they do not, it fails — at the file operation, the same way any unauthorised write fails.

"Who may install software" is therefore not a peipkg setting. It is just the access rules on the system directories — `Administrators`, by default. To grant someone install rights, you grant them write access to the directories packages land in; to scope what they may touch, you scope those security descriptors. This is the ordinary [access-decision](/peios/security-fundamentals/access-decisions/overview.md) machinery, with nothing package-specific layered on top.

One consequence follows directly: a package cannot grant its installer rights the installer did not already have. Any security descriptors a package asks to set on its own files can only be descriptors the caller already had the authority to set. There is no confused-deputy problem, because peipkg holds no authority that a package could misuse.

## Every change is a transaction

An install, an upgrade, a downgrade, a removal — each is a **transaction**, and each is atomic. There is a single instant, the **commit**, before which any failure, interruption, or power loss leaves the system exactly as it was, and after which the operation is complete. There is no partially installed intermediate state for a transaction to get stuck in.

Transactions are also reversible. peipkg keeps a history of them, retains the data needed to walk one back, and offers [`undo`](/peios/using-peios/package-management/keeping-a-system-current.md) and [`recover`](/peios/using-peios/package-management/transactions-and-recovery.md) to do it. [Transactions and recovery](/peios/using-peios/package-management/transactions-and-recovery.md) covers the model in full.

## Every operation is audited

peipkg records each operation it performs — what was installed, upgraded, or removed, and the outcome — to the Peios [audit stream](/peios/security-fundamentals/auditing/overview.md). When a plan contains an action that needs deliberate authorisation, the authorising act itself is recorded too.

These events are a semantic summary: a readable account of what peipkg set out to do. They are not the security boundary. The authoritative record is the kernel's own audit of the actual file operations — and because peipkg runs with no special authority, it cannot suppress that record.

## The command surface

Every command is invoked as `peipkg <command> [arguments]`.

| Command | Does |
|---|---|
| `install` | Install packages, with dependencies, from a repository or a local `.peipkg` file. |
| `remove` (alias `uninstall`) | Remove installed packages. |
| `upgrade` | Move installed packages to their newest available version. |
| `downgrade` | Move one package to a specific older version. |
| `undo` | Reverse the most recent transaction. |
| `claim` | Show or change which package holds a claim — a shared name that exactly one package may own. |
| `refresh` | Update the cached metadata of the configured repositories. |
| `repo` | Configure repositories — `add`, `list`, `remove`. |
| `root` | Manage named roots — `add`, `list`, `remove`, `show`. |
| `list` | List the installed packages. |
| `info` | Show one installed package's details. |
| `files` | List the files a package owns. |
| `owns` | Report which package owns a given path. |
| `search` | Search the configured repositories for a package. |
| `verify` | Check installed files against what was recorded at install. |
| `history` | Show the transaction log. |
| `recover` | Roll back a transaction left pending by an interruption. |
| `clean` | Delete cached metadata for repositories no longer configured. |

One global option sits before the command: `--root TARGET` makes peipkg operate on a Peios installation other than the running system at `/`. `TARGET` can be a literal path — a Peios installation mounted at `DIR`, the form used by image builders and offline maintenance — or the name of a [named root](/peios/using-peios/package-management/named-roots.md). Named roots are more than a convenience: they are how peipkg keeps components such as the initramfs current. See [Named roots](/peios/using-peios/package-management/named-roots.md) for the detail.

## The producer side

peipkg is the **consumer** half of the Peios packaging story — the tool that runs on a deployed system and consumes packages. It has a counterpart it never shares a process with: the **producer** tools (`peipkg-build`, `peipkg-repo`, `peipkg-manager`) that turn source into signed `.peipkg` files and assemble the repositories peipkg fetches from. Building packages and running a repository are a separate job with their own documentation, the *Peios Packages* guide. This topic is for the operator of a Peios system, not the operator of a build farm.

peipkg has one consumer-side companion of its own: **`peipkg-compose`**, a separate binary — not a `peipkg` subcommand. Where peipkg mutates a running system in place, `peipkg-compose` builds a fresh, package-owned root directory offline from a declarative manifest, the form image builders use when they assemble a system from nothing. It consumes the same packages peipkg does. See [Composing a root](/peios/using-peios/package-management/composing-a-root.md).

## Where to start

For the everyday work — putting software on a system and taking it off — read [Installing and removing packages](/peios/using-peios/package-management/installing-and-removing.md).

For keeping a system up to date, and for walking a change back, read [Keeping a system current](/peios/using-peios/package-management/keeping-a-system-current.md).

To configure where packages come from and how their authenticity is established, read [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

To understand why an interrupted install is always safe, read [Transactions and recovery](/peios/using-peios/package-management/transactions-and-recovery.md).

To understand how peipkg decides which versions of which packages a request implies — and why some actions require a second, separate authorisation — read [Dependency resolution](/peios/using-peios/package-management/dependency-resolution.md).

To understand how two packages that offer the same shared name settle on a single winner, read [Claims](/peios/using-peios/package-management/claims.md).

To inspect what is installed and check that it is intact, read [Inspecting and verifying](/peios/using-peios/package-management/inspecting-and-verifying.md).

For the more specialised work of operating on a Peios installation other than the running one — and how peipkg keeps things like the initramfs current — read [Named roots](/peios/using-peios/package-management/named-roots.md).

For the specialised work of assembling a fresh root offline from a manifest — the image-builder's path, run with the separate `peipkg-compose` binary — read [Composing a root](/peios/using-peios/package-management/composing-a-root.md).

---

# Installing and removing packages

_Peios / Using Peios / Package management_

> install puts packages on the system; remove takes them off. The plan-and-confirm flow, raw installs from a local .peipkg file, and cascade removals.

`peipkg install` puts packages on the system; `peipkg remove` takes them off. They are the two commands you reach for most, and they share one flow — peipkg works out the full set of changes, shows it to you, and waits for your approval before touching anything.

```
$ peipkg install nginx
$ peipkg remove oldtool
```

## Installing packages

```
peipkg install <package|file.peipkg>...
```

Each argument is either the **name** of a package to fetch from a configured repository, or the **path** of a local `.peipkg` file (recognised by its `.peipkg` suffix). You can mix the two in one command.

Installing a package rarely means installing just that package. peipkg works out everything the request implies — the dependencies the package needs, and the dependencies of those in turn — and presents the whole set. How that set is computed is the subject of [Dependency resolution](/peios/using-peios/package-management/dependency-resolution.md); this page is about the flow around it.

### The plan-and-confirm flow

`install`, `remove`, and the commands on [Keeping a system current](/peios/using-peios/package-management/keeping-a-system-current.md) all work the same way. peipkg first produces a **plan** — the ordered list of changes that satisfy your request — and prints it:

```
$ peipkg install nginx
the following changes will be made:
  install    pcre2 10.44
  install    zlib 1.3.1
  install    nginx 1.27.4
proceed? [y/N]
```

Nothing has been downloaded and nothing on the system has changed. peipkg waits for an answer. Anything other than `y` or `yes` — including pressing Enter, or end-of-input — is a refusal, and the command exits having done nothing.

Answer `y` and peipkg carries the plan out as a single [transaction](/peios/using-peios/package-management/transactions-and-recovery.md): it downloads and verifies every package, then commits the change atomically.

| Option | Effect |
|---|---|
| `--dry-run` | Produce and print the plan, then stop — never prompt, never change anything. |
| `--yes`, `-y` | Skip the `proceed?` prompt and apply the plan. |
| `--no-claim` | Install a provider without taking any claim it offers. |
| `--allow-stale` | Proceed although a repository's trust state exceeds its maximum trusted age. See [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md). |
| `--claim <names>` | Comma-separated claims to force-claim, overriding the current holder(s). |
| `--claim-all` | Force-claim every claim the installed packages provide, overriding incumbents. |
| `--dangerously-bypass-path-restrictions` | Permit packages that declare `special_system_package` to install outside the payload layout rules. Exempts nothing that has not declared itself special, and never reaches `/lcl/policy`. Needed only for the handful of packages whose job is to lay down the filesystem structure those rules protect. |

`--dry-run` is the safe way to see what a command would do. `--yes` is for scripts and unattended runs — but note that it skips only the routine prompt. A plan that contains an action needing deliberate authorisation will still stop and ask; `--yes` does not override that. See [Elevated authorisation](/peios/using-peios/package-management/dependency-resolution.md) for which actions those are and why.

`--claim-all` cannot be combined with `--claim` or `--no-claim`. Claims — shared names exactly one package may hold — are covered in [Claims](/peios/using-peios/package-management/claims.md).

An install can also target a root other than the current one — either explicitly with `--root`, or because a package declares its own default root. See [Named roots](/peios/using-peios/package-management/named-roots.md) for how roots are named and nested.

### Installing from a local file

When an argument is a path ending in `.peipkg`, peipkg installs that file directly:

```
$ peipkg install ./nginx-1.27.4.peipkg
```

This is a **raw install**, and it differs from a repository install in one specific way: it skips the repository **trust layer**. There is no signed index to check the file against, no signing key to verify it under, and none of the freshness or rollback protection a repository provides. You are vouching for the file yourself.

Everything else still happens. The package format is fully verified: the archive structure, the manifest, the integrity manifest, and the hash of every payload file are all checked before anything is staged. A corrupt or truncated `.peipkg` is rejected in the same way as one from a repository. The file's dependencies still resolve normally against your configured repositories — a locally-installed package can pull in repository packages to satisfy what it needs.

A package supplied as an explicit local file takes precedence over any repository's version of the same package, so `install ./foo.peipkg` installs that file even if a repository offers `foo` too. In the plan, a local-file operation is marked so the choice is visible:

```
  install    nginx 1.27.4  (local file)
```

In the future, peipkg will be able to consult system policy to decide whether raw installs are permitted at all, and that gate will be configurable. For now a raw install is always allowed; the verification above is what stands behind it.

## Removing packages

```
peipkg remove <package>...
peipkg uninstall <package>...
```

`remove` and `uninstall` are the same command. Each argument names an installed package; peipkg plans the removal — the files to take off disk — and runs the plan-and-confirm flow described above.

A removal leaves shared directories in place and removes only the files the package owns. peipkg knows which files those are from its database, so a removal is clean and complete.

### Removing something that is depended on

peipkg will not, by default, leave the system inconsistent. If you ask to remove a package that another installed package depends on, the plan is refused: peipkg tells you what still needs it, and stops.

| Option | Effect |
|---|---|
| `--cascade` | Also remove every installed package that depends on the ones named. |
| `--dry-run` | Print the plan and stop. |
| `--yes`, `-y` | Skip the `proceed?` prompt. |

`--cascade` turns that refusal into a wider plan: peipkg computes the full set of packages that would be left with a broken dependency and adds them to the removal. The plan then shows everything that will be removed. Review it before approving, because a cascade can reach further than expected.

```
$ peipkg remove --cascade libfoo
the following changes will be made:
  remove     toolA 2.1
  remove     toolB 1.0
  remove     libfoo 3.3
proceed? [y/N]
```

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — or, for `--dry-run`, the plan was produced. A declined prompt is also `0`: nothing failed. |
| `1` | The operation failed — a package was not found, a dependency could not be satisfied, a download or verification failed, or a file operation was denied. |
| `2` | A usage error — an unknown command or a malformed option. |

---

# Keeping a system current

_Peios / Using Peios / Package management_

> refresh learns what is available; upgrade moves to it; downgrade and undo walk a bad change back. The routine update cycle and the recovery commands.

Keeping a Peios system current is two commands in sequence — `refresh` to learn what is available, then `upgrade` to move to it. The other two commands here, `downgrade` and `undo`, go the other way: they walk a change back when an upgrade turns out badly.

## Refreshing repository metadata

```
peipkg refresh [repository]...
```

peipkg plans against a local, verified copy of each repository's metadata. That copy does not update itself. `peipkg refresh` is what updates it: for every configured repository — or just the ones you name — peipkg fetches the current signed descriptor and index, verifies them, and replaces the cached copy.

```
$ peipkg refresh
refreshed "official"
refreshed "internal"
```

Refresh has two properties worth knowing:

- **Repositories are independent.** If one repository is unreachable or fails verification, peipkg reports it and carries on with the rest. One bad repository never blocks a refresh of the others.
- **A failed refresh changes nothing.** If a repository cannot be refreshed, peipkg keeps the metadata it already had. It never falls back to unverified or stale-but-unchecked content. The worst a failed refresh does is leave you on yesterday's view.

What "verified" means here — signing keys, freshness floors, the handling of an unsigned repository — is the subject of [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

Run `refresh` before an upgrade. An upgrade plans against whatever metadata is cached, so an upgrade without a recent refresh moves you to the newest version peipkg last heard about.

Skip it for long enough and peipkg stops waiting for you: an install, upgrade, or downgrade against a repository whose trust state has passed its **maximum trusted age** (30 days by default) refreshes that repository itself, and refuses to proceed against one that stays stale — unreachable, or frozen on an unchanging index — unless you pass `--allow-stale`. The bound and its configuration are covered in [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

## Upgrading

```
peipkg upgrade [package]...
```

With no arguments, `upgrade` considers every installed package and moves each one that has a newer available version forward to it. With one or more package names, it upgrades only those — and still pulls in any new dependencies they need.

```
$ peipkg refresh && peipkg upgrade
the following changes will be made:
  upgrade    zlib 1.3.1 -> 1.3.2
  upgrade    nginx 1.27.4 -> 1.27.5
proceed? [y/N]
```

`upgrade` uses the same plan-and-confirm flow as `install` — peipkg shows the full set of changes and waits for approval — and the same options:

| Option | Effect |
|---|---|
| `--dry-run` | Print the plan and stop. A good way to preview what an upgrade would move. |
| `--yes`, `-y` | Skip the `proceed?` prompt. |
| `--no-recurse` | Confine the upgrade to the current root only — disable the cascade into nested named roots. |
| `--allow-stale` | Proceed although a repository's trust state exceeds its maximum trusted age. Warned and audited. |

By default, when named roots are configured, `upgrade` **cascades**: it reconciles the current root and every named root nested under it, each as an independent continue-on-error transaction with its own summary. `--no-recurse` disables that and upgrades the current root alone. See [Named roots](/peios/using-peios/package-management/named-roots.md) for the named-roots model.

If there is nothing to do — every package already at its newest version — peipkg says so and exits.

## Downgrading

```
peipkg downgrade <package> <version>
```

`downgrade` moves one package to a specific older version. You name the package and the exact version you want:

```
$ peipkg downgrade nginx 1.27.4
```

Older versions are not in a repository's active index — that index lists current versions only. They live in its **archive index**, which peipkg fetches on demand when you ask for a version that is not current. The package must still exist in some configured repository's archive for the downgrade to be possible.

A downgrade is treated as a deliberate move. Going backward — onto a version that may have known issues a newer one fixed — is one of the actions peipkg flags for **explicit authorisation**: beyond the routine `proceed?` prompt, peipkg asks you to authorise the specific downgrade, and `--yes` does not stand in for that answer. See [Elevated authorisation](/peios/using-peios/package-management/dependency-resolution.md) for the full set of actions that work this way.

`downgrade` accepts `--dry-run`, `--yes`, `-y`, and `--allow-stale` with the same meaning as elsewhere.

## Undoing the last change

```
peipkg undo
```

`undo` reverses the most recent committed transaction. If that transaction installed a package, `undo` removes it; if it upgraded, downgraded, or removed packages, `undo` restores each one to the version it had before.

```
$ peipkg undo
undoing transaction 47 (upgrade nginx, zlib)
the following changes will be made:
  downgrade  nginx 1.27.5 -> 1.27.4
  downgrade  zlib 1.3.2 -> 1.3.1
proceed? [y/N]
```

Be precise about what `undo` is. It is not a rollback of committed state — the previous transaction happened and stays in the history. `undo` computes the inverse of that transaction and applies it as a new transaction of its own. The history grows; it does not rewind. That new transaction can itself be undone, and so on.

Because restoring an older version is a backward move, `undo` carries the same explicit-authorisation requirement as `downgrade` for any package it walks back. It accepts `--dry-run` and `--yes`, `-y`.

To undo something other than the most recent transaction, or to see the history `undo` works against, use [`peipkg history`](/peios/using-peios/package-management/transactions-and-recovery.md) — and revert a specific package directly with `downgrade`.

## The routine cycle

For day-to-day maintenance the loop is:

```
$ peipkg refresh        # learn what is available
$ peipkg upgrade --dry-run   # preview the move
$ peipkg upgrade        # apply it
```

`downgrade` and `undo` are the recovery commands — use them when an upgrade has brought in a change you want to remove. Every one of these commands is a [transaction](/peios/using-peios/package-management/transactions-and-recovery.md), so every one of them is atomic and itself reversible.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — including a dry run, a plan with nothing to do, and a declined prompt or authorisation (nothing failed). |
| `1` | The operation failed — a repository could not be refreshed, a repository's trust state exceeded its maximum age and a forced refresh could not clear it, resolution or a download or verification failed, a root in an upgrade cascade failed, there is no committed transaction to undo, or a command's arguments were wrong (`downgrade` without a package and version, an unparsable version, a malformed option). |
| `2` | A usage error before any command ran — no command, an unknown command, or a malformed global option (including a `--root` reference that does not resolve). |

---

# Repositories and trust

_Peios / Using Peios / Package management_

> The repo commands, the trust ceremony that anchors a repository to a signing key, key rotation, signature policy, priority, and freshness protection.

A **repository** is where packages come from: an HTTP or HTTPS location that serves `.peipkg` files alongside signed indexes describing them. A Peios system has zero or more repositories configured, and the `peipkg repo` commands manage that configuration.

Beyond configuration, this page covers **trust**: establishing that the packages a repository serves are genuinely the ones its operator published, and not something a network attacker or a compromised mirror substituted.

## What a repository serves

At its base URL a repository serves three signed documents:

- A **descriptor** — the repository's identity: the set of signing keys it uses, each with a status. This is the root of trust for everything else.
- An **active index** — the catalog of current package versions: names, versions, dependencies, hashes, and download locations.
- An **archive index** — the catalog of older versions, kept so a [downgrade](/peios/using-peios/package-management/keeping-a-system-current.md) can reach them. peipkg fetches it only on demand.

The descriptor and the indexes each carry a detached signature. peipkg verifies all of them; an unverifiable document is discarded, never used.

## Adding a repository

```
peipkg repo add <name> <base-url> --anchor <fingerprint>
```

`repo add` does two things: it records the repository in your configuration, and it runs the **trust ceremony** that anchors it.

```
$ peipkg repo add official https://pkgs.peios.org \
    --anchor ef86709c4b1d8a02e5f3c719d640aa8b7c2e9105f8d3b6470a1c2e9d8b5f3a04
added repository "official"
```

The `--anchor` value is a **signing-key fingerprint**, and it is the part of the command that establishes trust. peipkg cannot tell you whether a repository is genuine; only you can, by obtaining its fingerprint through a channel you trust (the project's website over HTTPS, a colleague, a printed reference) and supplying it here. The anchor is your out-of-band statement that the key is trusted.

Given an anchor, the ceremony runs:

1. peipkg fetches the repository's descriptor and its signing keys.
2. It checks the fetched keys against the anchors you supplied — a fingerprint must match bit for bit.
3. Only if the descriptor's signature verifies against an anchored key does peipkg accept it and record the trust state.

If the fetched descriptor does not verify against any anchor, `repo add` fails and leaves nothing behind — no half-added repository, no configuration file. You can supply `--anchor` more than once to trust several keys at first contact.

| Option | Default | Effect |
|---|---|---|
| `--anchor FINGERPRINT` | — | A signing-key fingerprint to trust. Repeatable. |
| `--priority N` | `50` | Resolution priority — a lower number wins. See below. |
| `--policy required\|optional` | `required` | Whether a valid signature is required. See *Signature policy*. |
| `--min-index-version N` | `0` | A freshness floor for the index. See *Freshness*. |
| `--max-trusted-age-days N` | `30` | How stale the repository's trust state may grow before operations force a refresh. See *Freshness*. |
| `--max-index-staleness-days N` | `90` | How old the index's own `generated_at` may be before operations force a refresh. See *Freshness*. |
| `--insecure` | off | Permit a plain `http://` base URL. |

`--insecure` exists for a repository on a trusted local network with no TLS. It lowers transport confidentiality and integrity; the package signatures still protect authenticity, but prefer `https://` whenever it is available.

## Adding a repository your system already carries

An image can ship a repository's configuration — its base URL, its policy, and its trust anchors — as a `.repo` file in `/conf/peipkg/` (stored at `/lcl/conf/peipkg/`, which is what that view exposes). A Peios installation medium does exactly that for the offline repository it carries.

Configuration alone is **not** trust. peipkg does not trust a configured repository on sight: until the ceremony has run there is no recorded trust state, and every operation skips the repository with a warning rather than quietly using it. The decision to trust a key is yours, never a default.

What a baked-in `.repo` file changes is only where the anchor comes from. It arrived with the image rather than being typed at the prompt, which is still out-of-band — it did not come from the repository it authenticates. So the ceremony can run against it without you retyping a 64-character fingerprint that is already on disk:

```
$ peipkg repo add peios-medium
added repository "peios-medium"
```

Given a name and nothing else, `repo add` reads that repository's existing configuration and runs the same ceremony as the two-argument form. It is still an explicit act — nothing here happens on its own.

Two differences from the full form are worth knowing:

- It refuses a configuration with no `trust_anchors` under the `required` policy, because there would be nothing to verify the descriptor against.
- A failed ceremony leaves the `.repo` file alone. The two-argument form wrote that file and removes it again on failure; here it came from somewhere else, and deleting another party's configuration because a ceremony failed would turn a retryable problem — a medium not mounted yet, a repository not published — into lost settings.

## How trust survives key rotation

A repository operator will, over time, rotate signing keys — retiring an old one, bringing a new one into use. If trust were pinned permanently to the fingerprint you first anchored, every rotation would break every consumer.

Rotation does not break trust, because the descriptor carries the keys. Each key in the descriptor has a status:

| Status | Meaning |
|---|---|
| `active` | In current use for signing. |
| `transitioning` | Being phased in or out — honoured until its stated expiry. |
| `revoked` | Withdrawn — never honoured again. |

Each time peipkg refreshes a repository, it verifies the new descriptor against the keys in the descriptor it already trusts, and then adopts the new descriptor's key set as the current trust state. A rotation that is itself signed by a still-trusted key propagates automatically: you anchored once, and the chain carries forward from there without you re-anchoring. A `revoked` key is dropped and never accepted again.

The anchor therefore matters only at first contact. After that, trust is a chain, and each refresh extends it.

## Signature policy

`--policy` chooses how strict peipkg is about signatures for this repository.

- **`required`** (the default) — every descriptor and index must carry a valid signature from a trusted key. An unverifiable document is rejected. This is the correct setting for any repository reached over a network.
- **`optional`** — combined with no trust anchors, this puts the repository into **unsigned mode**: peipkg fetches and uses its metadata and packages without cryptographic verification.

Unsigned mode is a deliberate escape hatch for a scratch repository on a build host, or a local mirror you fully control. Every operation that touches an unsigned repository prints a warning:

```
peipkg: warning: repository "scratch" is unsigned — its metadata and
packages are not cryptographically verified
```

In unsigned mode, only the transport stands between you and a substituted package. Do not use it for anything reachable from an untrusted network.

## Priority

`--priority` is a number — lower is stronger, default `50` — that decides which repository wins when **more than one offers the same package**. If both an `official` repository at priority `10` and an `internal` one at priority `20` carry `nginx`, the resolver takes the `official` build. Priority does not restrict anything; it only breaks ties. Its full role in version selection is covered in [Dependency resolution](/peios/using-peios/package-management/dependency-resolution.md).

Priority also feeds one of the safety checks there: a package from a *lower*-priority repository that tries to displace a package installed from a *higher*-priority one is flagged for explicit authorisation, so a low-trust repository cannot quietly take over a package you got from a trusted one.

## Freshness — defeating a stale-repository attack

A signature proves a document is authentic; it does not prove the document is current. An attacker who cannot forge a signature can still serve you a genuine, correctly-signed, but old index — one from before a security fix was published — and a naive consumer would accept it.

peipkg closes that gap by tracking, per repository, the highest index version it has ever seen, and refusing any index that goes backward. Once you have seen version 5 of an index, version 4 — however well signed — is rejected. A repository can only ever move forward.

`--min-index-version N` lets you set that floor explicitly, out of band: if you know the current index is at least version `N`, anchoring that number means peipkg will reject anything older even on the very first fetch, before it has a history to compare against.

Going backward is one half of the attack; standing still is the other. A frozen repository — one that serves the same, correctly-signed index forever — never trips the rollback check, yet it can hold you on a view from before a security fix indefinitely. Against that, peipkg tracks when each repository last refreshed **with progress** and enforces a **maximum trusted age**: 30 days by default, tunable per repository with `max_trusted_age_days` in its `.repo` file or `--max-trusted-age-days` at add time.

When an install, upgrade, or downgrade finds a repository's trust state older than its maximum, peipkg refreshes that repository first. If it cannot — the repository is unreachable, or it refreshes without progressing — the operation is refused rather than planned against outdated metadata. Passing `--allow-stale` to the operation overrides the refusal; the override is warned about and recorded in the audit stream. A maximum above 180 days draws a warning on every operation, because a bound that loose effectively disables the check.

There is a third way to stand still, and it needs its own check. Maximum trusted age asks *how long since I last refreshed successfully* — and a repository can keep that answer flattering forever by bumping its index version on every publication while stamping the index with an ancient `generated_at`. Every refresh looks like progress; the metadata never actually moves. So peipkg separately enforces a **maximum index staleness** measured from the index's own `generated_at`: **90 days** by default, tunable per repository with `max_index_staleness_days` in its `.repo` file or `--max-index-staleness-days` at add time.

The two are deliberately independent, and raising one does not widen the other — a `max_trusted_age_days` of 180 still leaves the 90-day staleness window in place. An index past that window triggers a refresh before any install proceeds, and the same `--allow-stale` override applies, with the same warning and audit record. A staleness bound above 365 days draws a warning on every operation.

## Listing and removing repositories

```
peipkg repo list
peipkg repo remove <name>
```

`repo list` prints the configured repositories — name, base URL, priority, signature policy. It accepts `--json`.

```
$ peipkg repo list
official  https://pkgs.peios.org  priority=10  required
internal  https://pkg.corp.example  priority=20  required
```

`repo remove` deletes a repository's configuration and the trust state peipkg recorded for it. Packages already installed from that repository stay installed — removing a repository controls where future packages come from; it does not remove past ones. Those packages simply no longer have a source for upgrades until you add a repository that carries them again.

## Where the configuration lives

Each repository is one file: `/lcl/conf/peipkg/<name>.repo`, in flat TOML.

```toml
base_url         = "https://pkgs.peios.org"
priority         = 10
signature_policy = "required"
trust_anchors    = ["ef86709c4b1d8a02e5f3c719d640aa8b7c2e9105f8d3b6470a1c2e9d8b5f3a04"]
```

The files are hand-editable, and editing one is a legitimate way to configure a repository — a trust anchor written into the file is you supplying that anchor out of band. `peipkg repo add` is the convenient front end: it runs the ceremony and writes the file for you. Who may edit these files is, like everything else on Peios, the [security descriptor](/peios/security-fundamentals/security-descriptors/overview.md) on `/lcl/conf/peipkg/`.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — the repository was added, listed, or removed. |
| `1` | The operation failed — the trust ceremony failed (`repo add` backs out and leaves nothing behind), or the subcommand was missing, unknown, or given the wrong arguments (a missing `repo add <name> <base-url>`, a malformed option). Removing a name that is not configured is not a failure. |
| `2` | A usage error before any command ran — no command, an unknown command, or a malformed global option. |

---

# Transactions and recovery

_Peios / Using Peios / Package management_

> Every peipkg change is an atomic, reversible transaction. This page covers the three phases, the commit instant, interrupted runs, and recover and history.

Every change peipkg makes — an install, an upgrade, a downgrade, a removal — is a **transaction**. A transaction is atomic: it either happens completely or not at all. There is no state in which a package is half-installed, and no failure, signal, or sudden power loss that can leave one there.

This page explains how that guarantee is built, what an interrupted run leaves on disk, and the two commands — `recover` and `history` — that exist because of it.

## The three phases

Every operation moves through the same three phases.

```mermaid
flowchart LR
    P["Plan<br/>read-only, no lock"] -->|"you approve"| S["Stage<br/>lock held; fetch + verify"]
    S --> C["Commit<br/>the atomic flip"]
    C --> D["Done"]
    P -.->|"abort"| X["Nothing changed"]
    S -.->|"abort / crash"| X
```

**Plan.** peipkg reads your request, reads the installed set, reads the cached repository metadata, and computes the ordered list of changes — or rejects the request. This phase is entirely read-only and takes no lock. It is why `--dry-run` and the query commands never block, even while another transaction is running, and why a plan can always be abandoned for free.

**Stage.** Once you approve the plan, peipkg takes a single-writer lock — only one transaction touches the system at a time — and prepares everything. It downloads every `.peipkg` in the plan and **verifies all of them before staging any one of them**, so no package's contents can influence another's verification. Verified payloads are written into a staging area, each file hash-checked as it lands. Nothing the system uses has changed yet.

**Commit.** peipkg moves the staged files into place and records the new package state. This is the phase that changes the system.

## The one instant that matters

Within the commit there is a **single instant** — the moment peipkg records the new state in its database — that divides the entire operation in two:

- **Before it:** any failure, any signal, any power loss rolls everything back. The staged files are discarded, any displaced files are put back, and the system is exactly as it was.
- **After it:** the operation is complete and durable.

There is no third outcome. A transaction is never "partly applied". The rest of this page describes the machinery that delivers this guarantee.

## Backups make rollback free

When peipkg replaces or removes a file, it does not overwrite or delete it. It **renames the old file aside** — to a sibling name in the same directory — and puts the new file in place. The old contents are untouched, just under a different name.

Rolling back is then simply renaming everything back. No data is copied, no contents are reconstructed; the rollback is the same cheap rename operation in reverse. This is why a failed transaction recovers to a byte-exact prior state.

Those set-aside files also outlive a successful commit for a while — retained so that an [`undo`](/peios/using-peios/package-management/keeping-a-system-current.md) of a recent transaction is fast and needs no network. They are cleaned up automatically as they age out.

## What an interruption leaves behind

If a transaction is interrupted mid-stage or mid-commit, you may find files with these names near where a package was being installed:

| Name | Is |
|---|---|
| `<name>.peipkg-staged-<id>` | An incoming file that had been staged but not yet moved into place. |
| `<name>.peipkg-backup-<id>` | A file that had been renamed aside to make room — the backup. |

These names are deliberate. They have no leading dot, so they are visible rather than hidden — you are meant to find them and understand them. The `<id>` is the transaction number; look it up with `peipkg history` to see which operation left it.

You do not clean these up by hand. peipkg knows about them and resolves them itself — see `recover`, next.

## Recovering an interrupted transaction

```
peipkg recover
```

When a transaction is interrupted before it commits, peipkg records it as **pending**. The next time peipkg starts any transaction it checks for a pending one first and rolls it back automatically before doing anything else — so in normal use recovery just happens, and you never see it.

`peipkg recover` runs that step on demand. Use it when a transaction was interrupted and you want the system put right immediately, without waiting for the next install or upgrade.

```
$ peipkg recover
recovered: the interrupted transaction was rolled back
```

Recovery only ever **rolls back**. A transaction that was interrupted after its commit instant is already complete — there is nothing pending and nothing to recover. Recovery deals exclusively with the "before the instant" case, and it always resolves it the same way: back to the prior state.

If there is no pending transaction, `recover` says so and exits cleanly.

### Across more than one root

When an operation spans more than one named root, those roots commit as a unit under a single **cross-root transaction**, and `undo` reverses all participating roots together. `recover` reconciles pending cross-root transactions across every reachable root: a torn commit is rolled back, or — if it had already passed the commit instant — rolled forward to completion. See [Named roots](/peios/using-peios/package-management/named-roots.md) for how multiple roots come about.

## The transaction log

```
peipkg history
```

`history` prints the transactions peipkg has carried out, most recent first — each with an id, a timestamp, its state (`committed`, `rolled-back`, or `pending`), and a short summary.

```
$ peipkg history
49  2026-05-19T14:02:10Z  committed    upgrade nginx, zlib
48  2026-05-19T09:31:55Z  committed    install nginx
47  2026-05-18T22:14:03Z  rolled-back  install brokenpkg
```

| Option | Effect |
|---|---|
| `-n N` | Show at most `N` transactions. `-n 0` shows all of them. The default is `20`. |
| `--json` | Emit JSON. |

The history is what [`undo`](/peios/using-peios/package-management/keeping-a-system-current.md) reads to find the most recent transaction, and what ties a stray `*.peipkg-backup-<id>` file back to the operation that created it.

## Configuration files on upgrade

Upgrading a package raises a question for any configuration file under `/etc/` that the package owns: the new version ships a new default, but you may have edited the old one. peipkg decides per file, by comparing the file on disk against the hash it recorded at install:

- **Unchanged since install** — peipkg replaces it with the new default. You wanted the package's settings, and you get the current ones.
- **Edited since install** — peipkg keeps your file untouched and writes the new default beside it as `<name>.peipkg-new`. The upgrade report notes it:

  ```
  peipkg: warning: /etc/nginx/nginx.conf has been modified since install —
    keeping it; the new default was written to /etc/nginx/nginx.conf.peipkg-new
  ```

Your edits are never silently discarded, and the new defaults are never silently lost — you are simply told the two diverged and left to merge them when you choose.

## Side effects run after commit

A few packages need a system-wide step after their files are in place — refreshing the shared-library cache, the kernel-module map, or the manual-page index. peipkg runs those steps after the commit instant, once per transaction.

Because they run after the operation is already complete and durable, a side-effect step that fails is reported as a warning, not a failure. The transaction stands; the step is one that corrects itself the next time it runs. An install is never rolled back over a stale cache.

## Why this shape

The pay-off of the three-phase model is concrete:

- An install interrupted by a crash or a power cut never leaves a broken package — it leaves either the old state or the new one.
- The plan you approve under `--dry-run` is the plan that executes — resolution is read-only and deterministic.
- Queries and dry-runs never wait on an in-flight transaction, because only staging and commit take the lock.
- Every transaction is reversible, by `undo` for a recent one or `downgrade` for a specific package.

The model rests on one principle: all the fallible work — downloading, verifying, staging — happens before the commit instant, so the commit itself is the only point at which the system changes state.

## Where to go next

For the commands that reverse a committed transaction, read [Keeping a system current](/peios/using-peios/package-management/keeping-a-system-current.md).

For how a transaction that spans several named roots commits and recovers as a unit, read [Named roots](/peios/using-peios/package-management/named-roots.md).

For the events every transaction emits, read [Auditing](/peios/security-fundamentals/auditing/overview.md).

---

# Dependency resolution

_Peios / Using Peios / Package management_

> How a request becomes a concrete plan — dependencies, conflicts, provides and replaces, version choice, and the elevated-authorisation prompt.

When you ask peipkg to install `nginx`, the request does not yet describe the full work. `nginx` needs other packages; those need others; some version of each has to be chosen; everything has to be installed in an order where a package's dependencies come before it. Turning the short request into that concrete, ordered plan is **resolution**, and it is the first thing every change command does.

## Resolution is a pure calculation

peipkg resolves from **metadata alone** — the package descriptions in the cached repository indexes and in installed packages' manifests. It does not download any `.peipkg` files to work out a plan. Downloading happens later, only once a plan is approved.

That makes resolution a pure calculation over three inputs — your request, the set of installed packages, and the set of available packages — and gives it a property worth relying on: it is **deterministic**. The same inputs always produce the same plan. The plan you inspect with `--dry-run` is the plan that will execute; nothing is re-decided between previewing and applying.

If a request cannot be satisfied — a dependency that no repository can provide, two requirements that contradict — resolution rejects it, with an explanation, before anything is fetched or changed.

## The relationships between packages

A package's manifest declares how it relates to others. Four relationships drive resolution.

**Dependencies.** A package can require other packages, each by a version range. peipkg pulls every dependency into the plan, and their dependencies in turn, until the request is closed.

**Conflicts.** A package can declare that it cannot coexist with another. If a plan would put two conflicting packages on the system at once, resolution rejects it.

**Provides.** Several packages can advertise the same capability — a **virtual** name that is not itself a package. A dependency written against that name is satisfied by any package that provides it. This is how "needs a mail transport agent" can be met by whichever one you actually install.

You can ask for a virtual name directly, too: `peipkg install coreutils` works even when nothing is named `coreutils`, and installs whichever package provides it. Because you asked for one name and got a package with another, peipkg says which one it chose. Upgrades and removals are the exception — those name a package you already have, so they match by name only and never substitute one package for another.

**Replaces.** A package can declare that it supersedes another — the usual case being a rename, or a merge of two packages into one. Installing a package that `replaces` another causes the replaced package to be removed as part of the same plan.

`provides` has a stronger cousin: a **claim**, a single shared name that exactly one package may hold at a time. Where any number of packages can advertise the same `provides` name at once, a claim has one holder — see [Claims](/peios/using-peios/package-management/claims.md).

A dependency can also be routed into a different root, written `Depends: foo IN <root>`, so that a whole root can be composed through the dependency graph — see [Named roots](/peios/using-peios/package-management/named-roots.md).

## Choosing a version

When more than one version of a package could satisfy the plan, peipkg chooses by a fixed rule:

1. Prefer the highest version that satisfies every constraint on that package.
2. When two repositories offer the same version, prefer the one with the stronger (lower-numbered) **priority** — see [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

When the candidates are competing to fill a *virtual* name rather than being versions of one package, "highest version" means the version each one advertises **for that name**, not its own. Two packages providing `coreutils` are unrelated software numbered on unrelated scales, so peipkg compares what each offers the role — otherwise the role would go to whichever project numbers its releases higher. If everything above still ties, package name order settles it, so the answer never depends on the order packages happened to be read in.

For an upgrade, the installed version anchors the search: peipkg looks for something newer and stays put if there is nothing newer to move to.

**Optional dependencies** are never chosen automatically. A package can suggest companions that are useful but not required; peipkg surfaces those as suggestions and leaves the decision to you. A plan only ever contains what is genuinely needed plus what you asked for.

## The plan

The output of resolution is the ordered list of operations you see at the `proceed?` prompt — installs, upgrades, downgrades, and removals, sequenced so every package's dependencies are in place before it.

A removal gets one extra step. peipkg first computes the **reverse-dependency closure** — everything that would be left needing a package you are removing — and either refuses the plan or, with `--cascade`, widens it to include them. That logic is covered in [Installing and removing packages](/peios/using-peios/package-management/installing-and-removing.md).

Resolution is also **bounded**. A dependency graph cannot be crafted to make peipkg search forever; the resolver works under a hard cap and gives up cleanly rather than hanging if a graph is pathological.

## Elevated authorisation

Most of a plan is routine, and the single `proceed?` prompt covers it. A plan can also contain an action that a routine yes should not cover — one you must review and approve individually, on its own.

peipkg detects three such actions and, for each one in a plan, asks a separate question:

```
this operation requires elevated authorisation:
  nginx would move backward from 1.27.5 to 1.27.4
authorise this specific action? [y/N]
```

The three actions that trigger it:

| Action | Why it is elevated |
|---|---|
| **A downgrade** | Moving a package backward can reintroduce a problem a newer version fixed. It should be a conscious choice, never a side effect of some larger plan. |
| **A foreign `replaces`** | A package from a *lower*-priority repository using `replaces` to displace a package you installed from a *higher*-priority one. Left unguarded, a low-trust repository could quietly take over a package you trusted a better source for. |
| **A low-trust `provides`** | A package from a low-trust repository advertising a virtual capability in a way that shadows a package from a more-trusted source — satisfying a dependency with its own build instead of the one you would expect. |

Two things make this prompt different from the routine one:

- **`--yes` does not satisfy it.** `--yes` skips the routine `proceed?` prompt; it has no effect on an elevated-authorisation question.
- **It fails closed.** With no terminal to answer on — an unattended script, a pipeline — there is no answer, so the action is not authorised and the operation is cancelled. An elevated action is never authorised by default.

Each elevated action is presented and authorised on its own; approving one does not approve the next. And the authorising act is itself written to the [audit stream](/peios/security-fundamentals/auditing/overview.md) — the record shows not just what was done, but that it was specifically authorised and what was authorised.

If an elevated action is not one you want, the plan as a whole is the thing to reconsider: where the package is coming from, whether the repository priorities are right, whether the downgrade is really what you meant.

## Where to go next

For the flow that presents and applies the plans resolution produces, read [Installing and removing packages](/peios/using-peios/package-management/installing-and-removing.md).

For the single-holder extension of provides, read [Claims](/peios/using-peios/package-management/claims.md).

For the repository priorities and trust states resolution consults, read [Repositories and trust](/peios/using-peios/package-management/repositories-and-trust.md).

---

# Claims

_Peios / Using Peios / Package management_

> A claim is a shared filesystem name that only one installed package may hold. How claims materialise as symlinks, and the claim command for reassigning them.

Some filesystem names can be provided by more than one package. Two registry daemons — `loregd` and an alternative — both install a working binary, but only one of them can own `/usr/bin/registryd`. peipkg calls that shared name a **claim**, and it owns the machinery that decides which package holds it.

Three words are used precisely on this page:

- A **claim** is a single shared filesystem name that many installed packages may be able to answer, but exactly one may hold at a time.
- An **eligible provider** is an installed package able to answer a given claim.
- The **holder** is the one package that currently answers it.

(This is unrelated to [token claims](/peios/security-fundamentals/identity/claims.md) — the security-token attribute concept in the identity docs. The same word names a different subsystem.)

## What a claim is

A claim materialises as a **symlink** on disk. The link lives at the **claim path** — the shared name, such as `/usr/bin/registryd` — and points at a file inside the holder's payload, the **target**, such as `/usr/sbin/loregd`. Ask the system for the shared name and you reach whichever provider currently holds it.

```
/usr/bin/registryd -> /usr/sbin/loregd
```

The claim symlink is **owned and managed by peipkg**. It is not shipped inside any package's payload; no provider installs it, and removing a provider does not remove it out from under peipkg. peipkg creates, repoints, and tears down the link as part of the transactions that install, remove, grant, and revoke.

The two sides of a claim are declared in package manifests:

- A **provider** package declares the **target** file it offers for a claim — the real binary the shared name would resolve to.
- A **consumer** package references the **claim path** where it expects the shared name to appear.

peipkg joins the two. A provider can offer a target for a claim, a consumer can depend on the name being present, and peipkg mediates between them, keeping exactly one provider wired to the shared path at any moment.

## Claims and provides/replaces

A claim is the "exactly one owner of a shared name" extension of the `provides` and `replaces` relationships covered in [Dependency resolution](/peios/using-peios/package-management/dependency-resolution.md). There, several packages can advertise the same *virtual* name and any one of them satisfies a dependency written against it — that mechanism answers "is something here that provides this?". A claim goes one step further: it answers "which single provider owns this concrete filesystem name right now?", and enforces that the answer is never more than one. Provides establishes that a name can be answered; a claim decides which package answers it on disk.

## Auto-claim on install

Installing an eligible provider auto-claims every claim it provides that is currently unheld. If no package yet holds `registryd`, installing `loregd` makes `loregd` the holder and materialises the link as part of the same transaction — you get a working shared name without a second step.

The rule is strictly unheld-only. Auto-claim never overrides a claim that is already held by another package. Install a second registry daemon while `loregd` holds `registryd` and the newcomer is installed as an eligible provider but takes nothing; `loregd` stays the holder. Reassigning a held claim is always a deliberate act — see the `claim` command below, or the install flags that force it.

### Install flags

`peipkg install` accepts flags that override the default auto-claim behaviour for the packages in that install:

| Option | Effect |
|---|---|
| `--no-claim` | Claim nothing. Install the provider(s) without taking any claim, even ones that are currently unheld. |
| `--claim <names>` | Force-claim the named claims (comma-separated), overriding the current holder of each. |
| `--claim-all` | Force-claim every claim the installed packages provide, overriding incumbents. |

Two combinations are hard errors:

- `--claim-all` together with `--claim`.
- `--claim-all` together with `--no-claim`.

`--claim` and `--claim-all` are how you take a claim that is already held during an install; without them, an install only ever fills claims that are empty.

## The claim command

`peipkg claim` inspects a claim and reassigns its holder. It has three forms.

```
peipkg claim <claim>
peipkg claim <claim> grant <package>
peipkg claim <claim> revoke
```

**Report status.** With just a claim name, peipkg prints the current state of the claim: the current holder, the materialised links shown as `path -> target`, and the installed eligible providers.

```
$ peipkg claim registryd
holder: loregd
links:
  /usr/bin/registryd -> /usr/sbin/loregd
eligible providers:
  loregd
  altregd
```

**Grant.** `grant <package>` makes an installed eligible provider the holder. peipkg atomically repoints all of the claim's links to that package's targets — every path the claim covers moves together, or none does. The named package must be an installed eligible provider for the claim.

```
$ peipkg claim registryd grant altregd
```

**Revoke.** `revoke` removes the grant. The claim becomes **unheld** and its links are torn down. peipkg does not automatically promote another provider — a revoked claim has no holder until you grant one.

| Option | Effect |
|---|---|
| `--yes`, `-y` | Skip the confirmation prompt. Applies to `grant` and `revoke`. |

`grant` and `revoke` each run as a standalone transaction. Like every other peipkg change they appear in [`history`](/peios/using-peios/package-management/transactions-and-recovery.md), can be reversed with [`undo`](/peios/using-peios/package-management/keeping-a-system-current.md), and are fully auditable — each emits a `claim` event to the [audit stream](/peios/security-fundamentals/auditing/overview.md). A reassignment is never a silent, unrecorded edit to a symlink; it is a first-class, reversible operation.

## What happens on uninstall

Uninstalling the current holder **auto-withdraws** the claim. The holder is going away, so peipkg tears down its links and the claim becomes **unheld** as part of the removal.

peipkg does not auto-promote another provider in its place — an automatic promotion would be the kind of silent reassignment claims exist to prevent. Instead it surfaces the remaining eligible providers and hands you a ready-to-run command to reassign the claim yourself:

```
$ peipkg remove loregd
...
claim 'registryd' is now unheld. eligible providers: altregd
to reassign it, run:
  peipkg claim registryd grant altregd
```

If the holder was the only eligible provider, the claim is left unheld with nothing to promote, and any consumer relying on the shared name will find it absent until a new provider is installed.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The operation succeeded — the status was reported, or the grant or revoke was applied (a declined prompt is also `0`: nothing failed). |
| `1` | The operation failed — the claim has no eligible provider, the named package is not an eligible provider, or a named package or claim is not installed. |
| `2` | A usage error — an unknown subcommand (the only subcommands accepted after the claim name are `grant` and `revoke`) or a malformed option. |

---

# Inspecting and verifying

_Peios / Using Peios / Package management_

> The read-only commands — list, info, files, owns, and search to see what is installed and available, verify to check files are intact, clean to tidy the cache.

The commands on this page change nothing. They report what is installed, look up a package in the repositories, check that installed files are still intact, and tidy the metadata cache. Use them to see what is on a system and to check that it is still as it should be.

## Listing what is installed

```
peipkg list
```

`list` prints every installed package — name, version, and architecture.

```
$ peipkg list
nginx  1.27.5  x86_64
pcre2  10.44   x86_64
zlib   1.3.2   x86_64
```

With `--json` it emits the same set as JSON, with each package's origin repository included.

## Showing one package's details

```
peipkg info <package>
```

`info` prints the full record of one installed package: its version and architecture, the repository it came from (or `(local file)` if it was installed from a `.peipkg` directly), when it was installed, and — from its manifest — its description, license, and homepage.

```
$ peipkg info nginx
name:         nginx
version:      1.27.5
architecture: x86_64
origin:       official
installed:    2026-05-19T14:02:10Z
description:  HTTP and reverse proxy server
license:      BSD-2-Clause
homepage:     https://nginx.org
```

## Listing the files a package owns

```
peipkg files <package>
```

`files` prints every filesystem object the package owns — the files, directories, and symlinks that were placed by its install and are tracked against it. This is the record peipkg uses to remove the package cleanly and to verify it.

```
$ peipkg files zlib
/usr/lib/libz.so.1
/usr/lib/libz.so.1.3.2
/usr/include/zlib.h
```

## Finding which package owns a path

```
peipkg owns <path>
```

`owns` is the reverse lookup: given a path, it reports which installed package placed it.

```
$ peipkg owns /usr/lib/libz.so.1
zlib
```

If no installed package owns the path, `owns` says so and exits non-zero. A path that nothing owns is either not part of any package or was created outside peipkg.

## Searching the repositories

```
peipkg search <term>
```

`search` looks through the configured repositories' active indexes for packages whose name or description contains the term, case-insensitively. It searches what is available, not what is installed — it is how you find a package before installing it.

```
$ peipkg search proxy
nginx     1.27.5  [official]  HTTP and reverse proxy server
haproxy   2.9.7   [official]  Reliable, high-performance TCP/HTTP load balancer
```

`search` reads the cached repository metadata, so run [`peipkg refresh`](/peios/using-peios/package-management/keeping-a-system-current.md) first if you want it to reflect the latest catalog. A repository with no usable cached metadata is skipped with a warning rather than failing the search. `--json` emits the matches as JSON.

## Verifying installed files

```
peipkg verify [package]...
```

`verify` checks that what is on disk still matches what was recorded when each package was installed. For every file a package owns it checks:

- a **regular file** — that it is present and its content still hashes to the recorded value;
- a **symlink** — that it is present and still points where it was recorded to point;
- a **directory** — that it is still present and still a directory.

With no arguments, `verify` checks every installed package. With package names, it checks only those.

```
$ peipkg verify nginx
nginx: /etc/nginx/nginx.conf has been modified since install
verify: 1 problem(s) found
```

A reported file is not necessarily a fault. A configuration file you edited on purpose will show up — `verify` is telling you the file diverged from the package's version. `verify` reports; it does not judge intent and it does not change anything.

If every checked file is intact, `verify` says so and exits `0`. If anything diverged, it lists each problem and exits non-zero — which makes it usable as a check in a monitoring script.

## Cleaning the metadata cache

```
peipkg clean
```

peipkg keeps a verified copy of each repository's metadata in a local cache. When you remove a repository, its cached metadata is no longer needed. `clean` deletes only those orphaned cache files — the ones belonging to repositories that are no longer configured.

```
$ peipkg clean
removed 2 orphaned cache file(s)
```

`clean` never touches the metadata of a repository that is still configured, so it cannot leave you needing a refresh. It is pure housekeeping, and it is optional; run it whenever you like.

## Exit status

| Code | Meaning |
|---|---|
| `0` | The command succeeded. For `verify`, every checked file was intact. |
| `1` | The command failed — a named package is not installed, a path is owned by nothing, or, for `verify`, at least one file had diverged. |
| `2` | A usage error — an unknown command or a malformed option. |

---

# Named roots

_Peios / Using Peios / Package management_

> peipkg can operate on several roots at once — named references, dotted composition, default_root, the cascading upgrade, and cross-root dependencies.

Every peipkg command runs against a **root** — a subtree it treats as a complete Peios installation. By default that root is `/`, the running system. The [`--root DIR`](/peios/using-peios/package-management/overview.md) global option points peipkg at a different one: an image mounted at `DIR`, an offline system under maintenance, a tree being built.

A single bare path is enough when there is exactly one alternate tree and you always spell it out in full. It stops being enough when a system is several cooperating trees that are built and kept current together. Peios's **dynamic initramfs** is the case that motivates this page: a real system root at `/` and an initramfs image at `boot/initramfs`, both assembled from ordinary packages, both upgraded by the same package manager. Passing `--root boot/initramfs` on every command that touches the image — and remembering that path everywhere — is the friction named roots remove.

So `--root` generalises. A root can **register** other roots under it by name, reference them by that name instead of a path, compose those names by dotting, install a dependency into a *different* root than its depender, and upgrade every nested root in one command. The mechanism is deliberately general — the initramfs is one arrangement it expresses, not a special case wired into the tool.

## What a named root is

A **named root** is a registered name for a subtree, recorded in the registry of the root it lives under. The name `initramfs` bound to the path `boot/initramfs` is a named root of `/`: it says "within this root, the name `initramfs` means the subtree at `boot/initramfs`, and that subtree is itself a root."

Two ideas follow from that.

**Every root has its own registry.** The bindings a root knows about are its own. `/` may register `initramfs`; what `initramfs` registers is recorded inside `initramfs`, not in `/`. Paths in a registry are stored **relative to the root that owns them**, so a registry travels with its tree — an image built in one place and mounted in another resolves the same way.

**References are resolved from the current root.** The current root is whatever `--root` points at (or `/` when it is omitted). A reference is resolved by starting there and walking its registry.

### Referencing by name

Once `initramfs` is registered under `/`, these two commands target the same tree:

```
$ peipkg --root boot/initramfs install live-boot
$ peipkg --root initramfs install live-boot
```

The second names the root instead of spelling out its path. The name is looked up in the current root's registry and resolved to the bound path.

### Dotted composition

Names compose by dotting. `initramfs.subroot` resolves left to right: from the current root, walk into `initramfs`'s registry, then from there into `subroot`. Each dot is one more step outward through one more registry.

```
$ peipkg --root initramfs.subroot install ...
```

Every dotted segment must match the grammar `[a-z0-9][a-z0-9_-]*` — a lowercase-alphanumeric lead followed by lowercase alphanumerics, hyphens, and underscores. A segment that names no registered root is a **hard error**, not a silent miss. A registry arrangement that loops back on itself is detected and reported as a **resolution cycle** rather than followed forever.

### Path or name: how `--root` decides

`--root` accepts either form and tells them apart by a single rule:

- **Any value containing `/` is a filesystem path** and is used exactly as before — `--root boot/initramfs`, `--root /mnt/image`, `--root ./tree`. This is unchanged behaviour.
- **A bare dotted identifier is a named reference** — `--root initramfs`, `--root initramfs.subroot` — resolved through the registries as above.

The `/` is the discriminator. If you mean a literal directory, include a slash (even `./name`); if you mean a registered name, use the bare identifier.

## The `root` command

`root` manages named roots. **Every subcommand operates on the current root's registry** — the registry of whatever `--root` points at. To manage the initramfs's own named roots, aim `--root` at the initramfs first.

```
peipkg root add <name> <path>
peipkg root remove <name> [--purge]
peipkg root list [--json] [--tree]
peipkg root show <reference> [--json]
```

`root` with no subcommand is a usage error — *a subcommand is required (add, remove, list, show)*. An unknown subcommand is a usage error too.

### `root add`

```
peipkg root add <name> <path>
```

Register a named root in the current root's registry. `<name>` is a single root segment — it must match `[a-z0-9][a-z0-9_-]*` and must not contain a dot; you register one name at a time, not a dotted chain. `<path>` is stored relative to the current root. There are no flags.

```
$ peipkg root add initramfs boot/initramfs
```

### `root remove`

```
peipkg root remove <name> [--purge]
```

Unregister a named root. By default this removes only the **registry entry** — the files on disk are left in place, so the subtree survives and can be re-registered. `--purge` additionally deletes the named root's filesystem tree.

| Option | Effect |
|---|---|
| `--purge` | Also delete the named root's filesystem tree, not just its registry entry. |

### `root list`

```
peipkg root list [--json] [--tree]
```

Print the current root's registry. Plain output lists the names registered directly under the current root.

| Option | Effect |
|---|---|
| `--json` | Emit JSON — for each entry its `name`, its stored `path`, its `resolved_path`, its `status`, and its `children`. |
| `--tree` | Recurse into each child's registry and print the whole nested arrangement. The recursion is cycle-guarded, so a registry that loops is reported rather than followed endlessly. |

### `root show`

```
peipkg root show <reference> [--json]
```

Resolve one reference and report on it. `<reference>` is either a dotted named-root reference or a path — the same path-or-name rule as `--root`. `show` reports the reference's resolved **path**, its **status**, and the number of packages installed in it.

The status is one of two words:

- **`present`** — the resolved path exists on disk and is a real root.
- **`dangling`** — the reference resolves through the registries, but the path it names is not there. The binding exists; the tree it points at does not.

| Option | Effect |
|---|---|
| `--json` | Emit the same report as JSON. |

`present` and `dangling` are the vocabulary the rest of this page uses for "the binding points at something real" versus "the binding is valid but its target is missing".

## Automatic target selection with `default_root`

A package's manifest can declare the root a top-level install of it should land in — its **`default_root`**. When you install such a package without saying where, peipkg reads that field and re-roots the install onto it for you.

```
$ peipkg install live-boot
```

If `live-boot`'s manifest sets `default_root: initramfs`, this installs into the `initramfs` root even though no `--root` was given. The package's manifest records where it belongs, so you do not have to specify it.

Two rules keep this predictable:

- **It applies only when no explicit `--root` was given.** Passing `--root` with any value, including a named one, states the target outright and suppresses `default_root` re-rooting entirely. An explicit root always wins.
- **The packages in one command must agree.** If the packages named in a single install declare two or more distinct default roots, peipkg cannot pick one and does not guess — the command is rejected as an error. Split it into one command per target.

`default_root` only chooses a target; it does not create or resolve anything the registry could not already reach. The chosen root is resolved through the registries like any other named reference.

## Cascading upgrade

By default `peipkg upgrade` reconciles the current root and every named root nested under it, recursively — the whole tree of roots, in one command. Run it against `/` on a dynamic-initramfs system and both `/` and `boot/initramfs` are brought current together.

Each root is upgraded as an independent transaction that continues on error: peipkg walks the tree, upgrades each root on its own, and prints a per-root summary. One root's failure does not abort the others — a broken upgrade in `initramfs` leaves `/`'s upgrade to complete and report normally, and vice versa. You get one summary per root and a clear picture of which succeeded.

Each of those per-root upgrades is an ordinary transaction with the full atomic guarantee of [Transactions and recovery](/peios/using-peios/package-management/transactions-and-recovery.md); the cascade runs several of them, it does not weaken any one.

| Option | Effect |
|---|---|
| `--no-recurse` | Confine the upgrade to the current root only. The cascade into nested roots is disabled. |

Use `--no-recurse` when you deliberately want to move just one root — for example to upgrade the initramfs on its own without touching `/`, by pointing `--root` at it and disabling the cascade.

## Cross-root dependencies

A dependency can be routed into a different root than the package that declares it. A manifest writes this with `IN`:

```
Depends: foo IN initramfs
```

This says "I depend on `foo`, and `foo` belongs in the `initramfs` root" — the root name is resolved through the registries from the depending package's root. It is what lets a whole root be composed out of ordinary packages through the dependency graph: a single package installed into `/` can pull the pieces of the initramfs into `initramfs` as its dependencies, so the image is built by the same resolver and the same packages as everything else. An image builder starting from nothing can assemble an entire multi-root arrangement like this offline, from a declarative manifest, with the separate `peipkg-compose` binary — see [Composing a root](/peios/using-peios/package-management/composing-a-root.md).

`install` is the only verb that crosses roots. When resolution produces a plan whose changes land in more than one root, that plan is committed as a **two-phase commit** across the participating roots, under a single generated **cross-root transaction id** that ties the per-root pieces together. Either the change lands in every participating root or in none — the multi-root plan is atomic as a whole, not merely per root.

A plan that reaches beyond the current root announces it before you approve:

- a `note: this also changes other roots: ...` line naming the other roots the plan touches, and
- a per-line `-> <root>` tag on each plan entry that lands outside the current root, so you can see at a glance which change goes where.

You are never taken across a root boundary silently; a cross-root plan always shows its routing.

## Cross-root undo and recovery

Because a cross-root install commits as a unit, it reverses as a unit.

**`undo`** on a cross-root transaction reverses all of its participating roots together. There is no way to walk back only the `/` half of a change that also touched `initramfs` — the cross-root transaction id binds the pieces, and `undo` reverses the whole. See [Keeping a system current](/peios/using-peios/package-management/keeping-a-system-current.md) for `undo` in general.

**`recover`** understands cross-root transactions too. It reconciles them across every reachable root, walking the registry outward from the current root to find each participant. A cross-root commit that was torn by an interruption is resolved the same way a single-root one is — with one addition that follows from the two-phase commit: a torn commit is rolled back if it was interrupted before its point of no return, or rolled forward to completion if it had already passed that point. Either way every participating root ends in the same, consistent state.

This is the single-root recovery model of [Transactions and recovery](/peios/using-peios/package-management/transactions-and-recovery.md) extended across roots: the same commit-instant guarantee, now applied to a commit that spans several trees at once. Read that page for the underlying transaction model this builds on.

## Exit status

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Failure — an unregistered name, a dangling or otherwise invalid reference, a resolution cycle, a per-root transaction that failed, or a cross-root commit failure. |
| `2` | Usage error — a missing or unknown `root` subcommand, a malformed option, or the wrong number of positional arguments. |

---

# Composing a root

_Peios / Using Peios / Package management_

> peipkg-compose builds a fresh, package-owned root from a declarative manifest — offline and deterministically. The lock and build verbs and the lock file.

`peipkg` mutates a running system in place: every change is an atomic, reversible [transaction](/peios/using-peios/package-management/transactions-and-recovery.md) against a live root. **`peipkg-compose`** is its counterpart on the assembly side. Instead of changing a system that already exists, it builds a fresh, fully package-owned root directory from nothing — offline, deterministically, and verifiably, with no live system involved anywhere in the process.

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

Given a declarative TOML **manifest** that names a package set and the repositories to draw it from, compose produces a populated root directory: package payloads laid out at their installed paths, a seeded peipkg state database at `var/lib/peipkg/db.sqlite`, and each repository written out as `lcl/conf/peipkg/<name>.repo`. The result is a legitimate peipkg-managed system — once booted it can manage itself, and repository trust bootstraps on its first `refresh`.

It is meant for **image builders** — an outer image-assembly tool, or a person standing one up by hand. Because it only ever writes inside the output directory and never touches the host `/`, it is safe to run on a non-Peios host, and even on a host of a different architecture from the image being built.

## The mental model

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

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

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

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

## `peipkg-compose lock`

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

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

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

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

## `peipkg-compose build`

```
peipkg-compose build <manifest> --out <dir> [--locked | --update]
                     [--dangerously-bypass-path-restrictions]
```

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

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

| Option | Effect |
|---|---|
| `--out <dir>` | The output root directory. Required. Must not already exist. |
| `--locked` | Require an existing lock and do **not** resolve. Fails if no lock is present. Fetches only package bytes, taking integrity from the lock's hashes — the air-gap-friendly path. |
| `--update` | Re-resolve from scratch, overwrite the lock, then build. |
| `--dangerously-bypass-path-restrictions` | Permit packages that declare `special_system_package` to compose payloads outside the payload layout rules. An image built from a package set including the base filesystem needs this; `peiso` passes it through from `bypass_path_restrictions` in the image's build spec, so the grant stays a visible decision of the image rather than something the composer assumes. |

`--locked` and `--update` are **mutually exclusive**.

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

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

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

## The manifest

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

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

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

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

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

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

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

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

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

### Top-level fields

| Field | Type | Required | Meaning |
|---|---|---|---|
| `schema` | int | yes | Manifest schema version. Must be `1`. |
| `arch` | string | yes | Target architecture; becomes the image's primary architecture. |
| `source_date` | string | yes | An RFC 3339 timestamp. Fixes every build-stamped time in the output, for reproducibility — the manifest's `SOURCE_DATE_EPOCH`. |
| `local_packages` | array of strings | no | Paths or globs of local `.peipkg` files. A bootstrap path for packages that live in no repository. |
| `[[repository]]` | array of tables | no | Package sources. Also written into the image as `.repo` files. |
| `[[root]]` | array of tables | no | Named roots, for a multi-root image. |
| `[[package]]` | array of tables | yes | The packages to install. Must be present and non-empty — an empty package set is an error. |

### `[[repository]]`

A repository table has the same shape as a [`.repo` file](/peios/using-peios/package-management/repositories-and-trust.md). These entries drive metadata fetch and trust verification during the build, **and** are written verbatim to `lcl/conf/peipkg/<name>.repo`, so the booted system inherits the same repositories and the same trust anchors.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `name` | string | yes | Repository name; also the `.repo` filename stem. |
| `base_url` | string | yes | Where the repository's index and packages are served from. |
| `priority` | int | no | Selection priority. Default `50`. |
| `signature_policy` | string | no | `required` (default) or `optional`. |
| `trust_anchors` | array of strings | no | Hex key fingerprints trusted to sign this repository's index. |
| `allow_insecure_transport` | bool | no | Permit non-TLS transport. Default `false`. |
| `min_index_version` | int | no | Reject an index older than this version. |

### `[[package]]`

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

| Field | Type | Required | Meaning |
|---|---|---|---|
| `name` | string | yes | The package to install. |
| `version` | string | no | A version **constraint** — a range like `">=1.27, <1.28"` or an exact pin like `"9.9.1"`. Omitted, or `"*"`, means any version and the resolver picks the newest. |
| `repository` | string | no | Pin the source repository. Must name a declared `[[repository]]`. |
| `root` | string | no | Target a declared `[[root]]` — like [`peipkg install --root`](/peios/using-peios/package-management/named-roots.md). Empty means the package's own `default_root` if it has one, otherwise the anchor root. |

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

### `[[root]]`

Each root table registers a [named root](/peios/using-peios/package-management/named-roots.md) in the built image.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `name` | string | yes | A single named-root segment matching `[a-z0-9][a-z0-9_-]*` — no dots. |
| `path` | string | yes | Location relative to the output root. May not be absolute, may not escape with `..`, and may not be `.`. |

## The lock file

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

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

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

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

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

An unattended compose run cannot authorise the elevated actions that the live consumer would stop and prompt for. Rather than fail the build, compose surfaces those as stderr warnings. See [Elevated authorisation](/peios/using-peios/package-management/dependency-resolution.md) for what those actions are.

## Named roots and claims in a composed image

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

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

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

## What ends up in the root

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

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

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

## What compose does not do

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

- **Not a full image builder.** It produces only a directory tree — no bootloader, kernel, initramfs contents, registry seed, or peinit wiring. Those belong to whatever assembles the image around it.
- **Not a live-system tool.** It never touches the host `/`. There is no three-phase transaction, no commit boundary, no rollback journal, and no crash-recovery — the output is disposable, and its only atomicity is the single whole-tree rename above.
- **Not the producer side.** It does not build or sign `.peipkg` files and it does not serve repositories — those are the separate `peipkg-build`, `peipkg-repo`, and `peipkg-manager` tools. Compose consumes ordinary peipkg packages and repositories unchanged, exactly as [PSPU §5](/peios/advanced-peios/pspu/package-format-and-repository-protocol/scope-and-roles.md) defines them.
- **No side effects.** `ldconfig`, `depmod`, and `man-db` are not run; no KACS security descriptors are applied; no audit events are emitted. A booted system runs those itself.

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

## Exit status

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