# peinit

> Technical Reference Manual — the init system and service manager: boot, the service model, identity, supervision, jobs and operations, and shutdown.

---

# 1.1 Overview

_Peios / Advanced Peios / peinit / Introduction_

> peinit is PID 1 and the only service manager on a Peios system — what makes it not systemd, the shape of the daemon, and what it is not.

peinit is PID 1. It is the only service manager on a Peios system: every
supervised process on the machine — a platform daemon, an application
service, a startup hook, a health probe, a job some other service asked
for on a user's behalf — is forked by peinit and watched by peinit until
it exits.

It is a single-threaded Rust process. That is the constraint the rest of
its design answers to. A blocking syscall in PID 1 stops everything:
child reaping, watchdog expiry, shutdown signals, the control socket. So
peinit keeps a complete in-memory model of every service it knows about,
reads the registry synchronously only twice — at boot and on an explicit
reload — and pushes anything that could block off the main loop into a
forked helper or a pollable descriptor.

## 1.1.1 What makes it not systemd

Two things, and both run deep enough to change the shape of the daemon.

**Services are securable objects.** A service carries a Security
Descriptor that says who may start it, stop it, query it, or reload it,
and peinit evaluates that descriptor against the caller's KACS token on
every control request. There is no "root can do anything" path, because
there is no root — there is a token, and AccessCheck is the only thing
that decides.

**Service identity is a token, not a user.** peinit never sets a UID, a
GID, or a Linux capability on a service process. It obtains a KACS token
— minted from its own for the platform daemons that start before an
authority exists, requested from authd for everything else — restricts
its privileges to what the definition asked for, and installs it on the
child before exec. Every service also carries a per-service SID derived
from its name, so two services sharing an identity are still
distinguishable to an access check.

Configuration follows from the second one: service definitions live in
the registry, under `Machine\System\Services\`, where they are protected
by the registry's own descriptors rather than by file permissions. There
are no unit files, no generators, and no translation layer.

## 1.1.2 The shape of the daemon

Boot is two phases with registryd as the boundary. Phase 1 is compiled
in and has no registry dependency at all: it confirms the root is
writable, mounts what is missing, restores entropy, settles the clock,
and starts registryd. Phase 2 reads the service graph out of the
registry and boots the system from it. When Phase 1 cannot complete,
there is no Phase 2 to fall back to, and peinit drops into a recovery
shell.

Once booted, peinit is an event loop over a handful of descriptors: a
signalfd, the control socket, the notify socket, one timerfd per armed
timer, a pidfd per supervised process, a pipe pair per service's output,
the registry's change-notification descriptor, and the JFS device. Work
arriving on any of them turns into an **operation** — a queued,
observable request to move a service through its state machine — and
executing an operation eventually forks a **job**.

Those two objects are how peinit stays comprehensible under concurrency.
Operations exist so that two administrators issuing conflicting commands
get a defined answer instead of a race. Jobs exist so that "what
actually ran" is a thing with an identifier, a token summary, an exit
status and a log correlation key, rather than a PID that may already
have been reused.

peinit keeps no history of either. A job or an operation that reaches a
terminal state is emitted as a structured event into the KMES kernel
ring buffer and dropped. eventd is the historian.

## 1.1.3 What peinit is not

It does not assemble storage. The initramfs delivers a mounted,
writable root, and peinit neither decrypts, nor assembles, nor checks
it. It has no mount feature beyond the fixed Phase 1 set — mounting a
data partition is a Oneshot service's job.

It does not store logs. It holds the pipes at birth, tags each line, and
forwards it to eventd; before eventd exists it buffers, and when the
buffer fills it drops the oldest.

It does not authenticate anyone. authd mints tokens; peinit installs
them. It does not resolve identities, and it does not know or care
whether a principal is local or from a domain.

And it does not support forking daemons. It tracks the process it
spawned, through a pidfd obtained at fork, and there is no mechanism for
a service to point supervision somewhere else.

---

# 1.2 What This Manual Covers

_Peios / Advanced Peios / peinit / Introduction_

> The scope of this manual, what in peinit is a contract and what is not, and where the constants and registry keys live.

This manual describes peinit as it is built: the boot sequence, the
service model and its registry schema, how a service acquires its
identity, the exact sequence between "start this" and "the binary is
running", the state machine and its causes, dependencies, jobs and
operations, timers, output handling, shutdown, and the security model.

## 1.2.1 What is a contract and what is not

Two of peinit's interfaces are specified separately, in PSPU §4: the
**control socket**, spoken by administrative tools and by any program
that manages services, and the **notification socket**, spoken by every
service that reports readiness, keepalives, or a stored descriptor.
Those are contracts. A third party implements one side of each, and what
is written there binds both.

This manual covers the other side of that boundary — how peinit fulfils
them, and everything that is not a contract at all:

- how a command becomes an operation, and what happens when two of them
  collide (§8.2, §8.3)
- what a command does to a service in each state (§10.3)
- how a notification is authenticated and applied (§10.5, §10.6)

Where a chapter touches the contract it references PSPU §4 rather than
restating it.

The **service definition schema** is here rather than in PSPU. It is
registry configuration, protected by the registry's descriptors and
administered by the same tools as the rest of the registry, and it is
documented for the people who write service definitions rather than
specified as a wire format. §3.2 is the reference.

## 1.2.2 What this manual does not cover

- **KACS** — tokens, Security Descriptors, AccessCheck, and the
  per-service SID algorithm peinit reproduces. Peios Kernel TRM §3.
- **LCS** — the registry syscalls, watches, and layer resolution peinit
  reads through. Peios Kernel TRM §5.
- **KMES** — the ring buffer peinit emits its events into. Peios Kernel
  TRM §2.
- **loregd** — the storage behind registryd. Its own TRM.
- **eventd** — where the logs and events go. Its own manual.
- **authd** — token minting and identity routing. peinit's requirements
  of it are described in §4.3; its interface is authd's own.
- **JFS** — the kernel side of ad-hoc job submission. §8.5 describes
  what peinit does with what JFS hands it.
- **Using peinit** — writing service definitions, the `svctl` command
  surface, and everyday administration are covered in Using Peios.

## 1.2.3 Constants and keys

Registry keys and compiled-in constants are collected in the appendices,
so a chapter's reference material does not interrupt the prose that
explains it. Where an appendix defines a value the body references it
rather than repeating it.

---

# 1.3 Terminology

_Peios / Advanced Peios / peinit / Introduction_

> The terms this manual uses without redefining them, and where each is introduced.

Terms defined where they are introduced — activation generation, boot
generation, transition cause, cgroup generation, start generation — are
not repeated here.

**Service.** A named, supervised unit of execution: a definition in the
registry, a runtime state, a Security Descriptor, and at most one
running main process. Services are the primary unit of management and
the thing dependencies are expressed between.

**Job.** One supervised process execution. Every fork peinit performs
is a job — a service's main binary, a pre-exec hook, a health check
invocation, an ad-hoc submission — with a GUID, a lifecycle, a token
summary and a log correlation key. Jobs are the observable unit of what
actually ran.

**Operation.** A first-class object representing a requested state
machine action on a service. Control commands do not mutate state
directly; they create operations that are validated, queued, resolved
against whatever else is in flight, and executed by the event loop.

**Trigger.** A rule in a service definition saying when the service
starts automatically: at boot, once the boot set has settled, or on a
schedule. A service with no triggers starts only when something asks
for it.

**Phase 1.** The compiled-in bootstrap: root writability, the remaining
virtual filesystems, the persisted random seed, the local machine ID,
the clock, registryd, boot-time path provisioning, and infrastructure
setup. No registry access happens before registryd is serving.

**Phase 2.** The registry-driven boot: read the service definitions,
build and validate the dependency graph, and start the boot-triggered
services in dependency order.

**ErrorControl.** The per-service policy for an irrecoverable failure.
Normal leaves the service Failed; Critical syncs the filesystems and
reboots.

**Token.** The per-thread KACS identity object — a user SID, group SIDs,
a privilege bitmask, an integrity level, and metadata. Tokens are the
sole identity mechanism: peinit sets no UIDs, GIDs, or Linux
capabilities on service processes. Peios Kernel TRM §3.2.

**Security Descriptor** (SD). The KACS structure controlling access to
a securable object. peinit uses two: a **ServiceSecurity** descriptor
per service, controlling who may manage it, and its own **control**
descriptor, controlling system-level operations. Peios Kernel TRM §3
and PCDS §5.

**AccessCheck.** The KACS function that evaluates a token against a
descriptor to produce a decision. peinit calls it for every control
request. Peios Kernel TRM §3.8.

**Per-service SID.** A SID under authority `S-1-5-80` derived
deterministically from the service name, carried in the group list of
every service token. Two services running as the same principal are
still distinguishable to an access check. §4.4.

**registryd.** The userspace registry source daemon serving the
persistent hives. peinit starts it in Phase 1 from a compiled-in
definition and treats it as opaque thereafter. Its implementation is
loregd, a distinction visible only in recovery mode.

**Registry.** The configuration system LCS and its sources provide
together. peinit reads service definitions from
`Machine\System\Services\`, boot configuration from
`Machine\System\Boot\`, and its own parameters from
`Machine\System\Init\`.

**KMES.** The kernel-mediated event subsystem. peinit emits its
structured events — job and operation lifecycle, audit records — into
its ring buffer, where they survive eventd restarts and reboots. Peios
Kernel TRM §2.

**JFS.** The Job Forwarding Subsystem: the kernel bridge that captures a
caller's effective token and delivers it, with a job definition, to
whatever holds `/dev/jfs` open. peinit is the consumer. §8.5.

**pidfd.** A descriptor referring to one specific process, obtained
atomically at fork through `clone3(CLONE_PIDFD)`. Every process peinit
supervises is tracked by one, which is what makes supervision immune to
PID reuse.

**cgroup.** peinit uses cgroups v2 for process tracking and clean kill
only — not for resource accounting or limits. Every service gets its own
tree under `/sys/fs/cgroup/peinit/`. §5.1.

**sd_notify.** The datagram protocol services use to report readiness,
status, keepalives and stored descriptors, over the socket named by
`NOTIFY_SOCKET`. Specified in PSPU §4.

**TCB.** The Trusted Computing Base: the kernel, KACS, LCS, KMES,
peinit, registryd, authd, lpsd and eventd. A compromise of any of them
compromises the system.

---

# 1.4 Compatibility and Prior Art

_Peios / Advanced Peios / peinit / Introduction_

> peinit is not a port of anything — where it meets sd_notify, the fd store, calendar expressions and the Windows SCM, and what it deliberately omits.

peinit is not a port or a reimplementation of anything. The choices that
give it its shape — identity as a token, configuration in the registry,
operations as objects, jobs as observable executions — were made for
Peios. But several interfaces deliberately match existing conventions,
because the convention is good and breaking it buys nothing.

## 1.4.1 sd_notify

peinit speaks systemd's sd_notify datagram protocol: a service reports
readiness, keepalives, status and stored descriptors by sending
`KEY=VALUE` lines to the socket named in `NOTIFY_SOCKET`. Existing
software that supports sd_notify works unmodified.

The compatibility is not total. `MAINPID=` is not supported, because
peinit does not supervise forking daemons — it tracks the process it
forked through a pidfd, and there is no way to redirect supervision
somewhere else. `BUSERROR=` is not supported because Peios has no D-Bus.

The protocol as peinit speaks it, including which fields it accepts and
how a sender is authenticated, is specified in PSPU §4.

## 1.4.2 The fd store

`FDSTORE=1`, `FDNAME=`, `FDSTOREREMOVE=1` and `FDPOLL=0` work as they do
under systemd, and descriptors come back to a restarted service at fd 3
onwards with `LISTEN_FDS` and `LISTEN_FDNAMES` set. A daemon written to
survive a restart without dropping its listening sockets keeps working.

## 1.4.3 Calendar expressions

Timer schedules use systemd's `OnCalendar` format, including weekday
names, lists, ranges, repetition, the `~` last-day-of-month form, IANA
timezone suffixes and the named shortcuts. §9.1 gives the grammar peinit
actually parses; the one deliberate subtraction is sub-second precision,
which service scheduling has no use for.

## 1.4.4 Windows Service Control Manager

The service model owes its architecture to the Windows SCM: services as
securable objects carrying their own descriptors, identity as a token
rather than a user account, an access-controlled control interface, and
a structured state machine. The debt is architectural. peinit implements
none of the SCM's RPC protocol, none of its service types, and none of
its control codes.

## 1.4.5 What is deliberately absent

There is no systemd unit file support: no reader, no parser, no
generator, no migration path. Service definitions are registry keys.
Roles are how a package declares them.

There is no socket activation in the systemd sense — peinit does not
listen on a service's behalf and hand it a connection. The fd store
covers the case that matters, which is a service keeping its own
listener across its own restart.

There is no resource control. peinit uses cgroups for tracking and for
clean kill, and sets `RLIMIT_NOFILE` and `RLIMIT_CORE` if a definition
asks, but it does not do accounting, slices, or limits.

## 1.4.6 Features that belong to other components

| Concern | Component |
|---|---|
| Authentication and token minting | authd |
| The local identity database | lpsd |
| Log storage, indexing and queries | eventd |
| Registry storage | registryd, over LCS |
| Packaging and installation | peipkg and the role system |
| Device management | eudev |
| Network configuration | a dedicated service |
| File access control enforcement | FACS |

---

# 2.1 The Initramfs Contract

_Peios / Advanced Peios / peinit / Boot_

> peinit starts with the root already assembled — what the handoff guarantees, and everything that stays outside it.

peinit starts with the root filesystem already assembled. Everything
that makes a root mountable — LUKS decryption, LVM activation, RAID
assembly, any filesystem check — belongs to the initramfs and has
happened before peinit exists. peinit performs no root assembly, no
decryption, no repair, and no fsck, and it does not assume one has been
done.

## 2.1.1 The handoff

The initramfs transfers control by `chroot`-ing into the assembled root
and exec'ing peinit there. Not `switch_root`, and not `pivot_root`: the
kernel refuses to relocate onto the initramfs rootfs, so that route is
closed. The consequence is that the initramfs rootfs does not go away.
It remains the mount-namespace root — emptied, and unreachable from
peinit's view, but present. peinit therefore never assumes a clean
single-root mount topology, and never attempts `pivot_root`.

peinit is installed in package storage at `/usr/bin/peinit2` and reached
through the fixed runtime path `/bin/peinit2`. The boot-image tooling
sets the kernel `init=` to the runtime path. Before the transfer, the
initramfs has assembled the base StrataFS topology, including the `/bin`
and `/sbin` views, because peinit reaches every binary it execs through
them.

At handoff:

- the real root is mounted **read-write** at `/`;
- `/proc`, `/sys` and `/dev` are mounted and have been moved into the
  real root;
- the environment holds `TERM` and nothing else, and argv is just
  peinit's own path.

The read-write requirement is registryd's, not peinit's. loregd's
storage backend needs to write its write-ahead log and shared-memory
files even to answer a read, so a read-only root cannot support Phase 2
at all. Delivering the root writable is the initramfs's job; peinit only
confirms it.

peinit inherits nothing from that environment. It does not rely on
having been given anything, and it does not pass its own near-empty
startup environment through to services — the environment a service
receives is constructed from scratch (§5.5).

## 2.1.2 What stays outside

Non-root storage is not peinit's concern. Data partitions and additional
filesystems are mounted at the services layer, typically by a Oneshot
service that runs `mount`. peinit has no mount feature beyond the fixed
Phase 1 set.

> [!NOTE]
> The initramfs itself is assembled by mkirf from hook scripts that
> packages contribute; mkirf resolves the ordering and bakes the
> sequence its PID 1 runs. What peinit depends on is the handoff
> contract above, not how the initramfs arranged to satisfy it.

---

# 2.2 Bootstrap Identity

_Peios / Advanced Peios / peinit / Boot_

> The steady-state identity flow cannot start the system, so platform services run as SYSTEM until authd exists.

The steady-state identity flow is: peinit asks authd for a token, authd
mints it, peinit installs it on the child. That flow cannot start the
system, because authd depends on lpsd, lpsd depends on registryd, and
registryd has to be running before any of them. The bootstrap model
breaks the circle.

## 2.2.1 Platform services run as SYSTEM

A service whose definition says `Identity=SYSTEM` gets a token peinit
mints from its own, with `kacs_create_token` (§4.2). No authd
interaction is involved — which is the point, since authd does not exist
when the first of these services starts.

Four services use it:

| Service | Why |
|---|---|
| registryd | Starts before authd exists at all. |
| lpsd | Must be running before authd can resolve a local identity. |
| authd | Needs `SeTcbPrivilege` and `SeCreateTokenPrivilege`; it is the minter for everything else. |
| eventd | Starts early, before authd is necessarily available. |

Nothing restricts which services may declare `Identity=SYSTEM`. There is
no allowlist, because an allowlist would be enforcing a boundary that is
already enforced somewhere better: the Security Descriptor on
`Machine\System\Services\`. Anyone who can create a service definition
is by definition trusted to choose its identity, and adding a second
list to maintain would only create a way for the two to disagree.

Every SYSTEM token peinit mints carries the service's per-service SID in
its group list, computed by peinit itself from the service name (§4.4).
That is what keeps platform services distinguishable to an access check
despite all of them running as `S-1-5-18`.

> [!NOTE]
> This is the same arrangement Windows uses: the SCM, LSASS and the core
> platform services all run as LocalSystem. The trust anchor is that
> peinit *is* SYSTEM, and the kernel guarantees that by handing PID 1
> the boot token.

## 2.2.2 After authd

Once authd and lpsd are running, every subsequent service gets its token
through the ordinary authd flow (§4.3). A definition with no `Identity`
field defaults to `LocalService` — a well-known principal with a minimal
privilege set — and authd adds the per-service SID to the token it
mints.

---

# 2.3 Phase 1

_Peios / Advanced Peios / peinit / Boot_

> The compiled-in phase with no registry dependency, whose whole purpose is to reach the point where a registry can serve.

Phase 1 is compiled into peinit. It does not change at runtime and has
no registry dependency, because its whole purpose is to reach the point
where a registry exists. It does the minimum needed to make Phase 2
possible, and most of its failures are fatal to the boot.

## 2.3.1 Step 1: Confirm the root is writable

The initramfs delivers the root mounted read-write. peinit does not
remount it — mount flags belong to the initramfs, and a redundant
remount of an already-writable or overlay root can fail for reasons that
have nothing to do with the root being usable.

Instead peinit probes. It creates `/.peinit/` if it is absent, writes a
uniquely named file there — the name is derived from peinit's own PID
and a namespace identifier, so two probes cannot collide — writes to it,
and removes it. If any part of that fails, the root is not usable for
Phase 2 and peinit enters recovery mode.

## 2.3.2 Step 2: Mount what is missing

`/proc`, `/sys` and `/dev` are already mounted. peinit does not
blindly mount them again: a redundant mount stacks a second filesystem
over the populated one, and on some kernel and flag combinations returns
`EBUSY` instead.

peinit reads `/proc/self/mountinfo` to find out what is already there,
and mounts only what is not:

| Mount point | Filesystem | Flags | Provided by |
|---|---|---|---|
| `/proc` | proc | nosuid, nodev, noexec | initramfs |
| `/sys` | sysfs | nosuid, nodev, noexec | initramfs |
| `/dev` | devtmpfs | nosuid | initramfs |
| `/dev/pts` | devpts | nosuid, noexec | peinit |
| `/dev/shm` | tmpfs | nosuid, nodev | peinit |
| `/run` | tmpfs | nosuid, nodev | peinit |
| `/sys/fs/cgroup` | cgroup2 | nosuid, nodev, noexec | peinit |

Each `mount(2)` passes the filesystem name as both the source and the
filesystem type, passes only the listed flags, and passes null mount
data. Mount points that do not exist are created first.

There is a bootstrap wrinkle in reading mountinfo at all: the file lives
in `/proc`, which is one of the things being checked for. If the read
fails with `ENOENT` or `ENOTDIR`, peinit mounts `/proc` from the table
and retries. Any other failure to read or parse mountinfo sends peinit
to recovery.

For the three initramfs-provided rows, an already-mounted filesystem is
success, and so is an `EBUSY` from an attempted mount. For the four
peinit owns, a mount failure sends peinit to recovery.

### 2.3.2.1 Seeding descriptors on the new filesystems

Three of the four filesystems peinit mounts are fresh and empty:
`/dev/shm`, `/run` and `/sys/fs/cgroup`. Under KACS an inode with no
Security Descriptor is denied to every caller, and there is nothing on a
newly mounted tmpfs for a new inode to inherit from — so peinit stamps
the mount root with a descriptor that grants SYSTEM and Administrators
full control and is marked inheritable by both containers and objects:

```
O:SY G:SY D:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)
```

Everything created underneath — the control socket, the notify socket,
per-service runtime directories, the cgroup hierarchy — inherits from
it. This is why peinit never sets mode bits on the sockets it creates;
under KACS they would mean nothing, and the descriptor is the thing that
does the work. It is the same descriptor the initramfs seeds onto the
root filesystem, and the two are kept identical on purpose: this one
inheritable ACL is, in practice, the access policy of everything under
these mounts, so an ACE missing here is missing from every per-service
directory under `/run/services`.

Failure to apply the descriptor sends peinit to recovery. Without it
every file peinit later creates on that filesystem would be unreachable
to everything, including peinit.

`/proc`, `/sys` and `/dev` are not stamped: they arrive from the
initramfs already populated.

### 2.3.2.2 Device node policy

`/dev` arrives with that same inheritable descriptor on its root and on
every node, which is the right default — whatever the root grants, a
disk hot-plugged later inherits, so the root must be acceptable on a raw
block device — but it leaves `/dev/null` unusable by anyone who is not
an administrator. A single inherited descriptor cannot say "`/dev/null`
for everyone, the disks for administrators", so peinit enumerates the
exceptions. Once the mounts are up it stamps each of these nodes with a
descriptor of its own:

| Node | DACL |
|---|---|
| `/dev/null`, `/dev/zero`, `/dev/full`, `/dev/random`, `/dev/urandom`, `/dev/tty`, `/dev/ptmx` | `D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FRFW;;;WD)` |

Everyone may open the node for reading and writing and may stat it;
nobody but SYSTEM and Administrators may change its descriptor. Only the
DACL is replaced — owner and group stay as the seed left them — and the
ACEs carry no inheritance flags, because a device node has no children.
`/dev/console` is deliberately not on the list: it is the SYSTEM
console.

The step is advisory. A node that cannot be stamped is reported as a
warning and stays on the inherited default, usable by administrators
and denied to everyone else; a node that does not exist is noted and
skipped. Neither sends peinit to recovery.

## 2.3.3 Step 3: Restore the persisted random seed

peinit restores the seed at `/var/state/peinit/random-seed` once `/dev`
is available and before registryd starts. The seed is a machine-local
entropy cache for the kernel CSPRNG. It is not configuration, and
shipping one in a packaged image, live ISO or VM template would hand
every instance of that image the same starting entropy.

If the file is absent, that is an ordinary first boot or a stateless
live boot, and peinit continues silently. When a seed is present peinit
mixes it into the kernel pool, preferring the interface that credits
entropy for a locally persisted seed; if that fails it mixes the bytes
without crediting and records the failure. A seed file that is empty, or
larger than 4096 bytes, is treated as an error.

Nothing in this step can send peinit to recovery. A system with no
entropy cache still boots; it just starts with less entropy, which is a
problem for the image builder to solve with a hardware or virtio RNG
rather than with a seed baked into the image.

The initramfs may perform the same restore earlier, once the persistent
root is mounted. peinit's restore stays as the fallback for initramfs
images that do not participate and for boots that have no initramfs.

## 2.3.4 Step 4: Ensure the local machine ID

`/lcl/etc/machine-id` holds a stable local install identifier used for
software compatibility, log correlation and instance identity. It is not
a security principal: not a credential, not a SID, not an account, and
not an input to any authorisation decision.

The format is 128 bits as exactly 32 lowercase hexadecimal characters
followed by one newline. A valid existing file is left alone. A file
that is absent, empty, all zeroes, the wrong length, not hexadecimal, or
missing its trailing newline is replaced: peinit draws 128 bits from the
kernel CSPRNG and writes a valid file atomically, through a temporary
file and a rename, with the result flushed.

Any failure of this step — an unreadable file, a CSPRNG failure, or an
unwritable path — sends peinit to recovery. The write does not create
parent directories, so an image that ships without `/lcl/etc/` present
fails here rather than at first use.

Images and templates are expected to ship with no machine ID, or with an
empty file as a reset marker. Clone tooling that wants a new identity
removes or truncates the file and lets the next boot generate one.
Stateless live boots without a persistent overlay get an ephemeral ID
for that boot.

## 2.3.5 Step 5: Set the clock from the hardware RTC

peinit reads the hardware clock and calls `clock_settime()` before
registryd starts, so that timestamps on registry operations, log entries
and the boot attempt counter mean something.

It opens `/dev/rtc`, falling back to `/dev/rtc0` if that device is
absent, and reads it with `RTC_RD_TIME`. The returned `struct rtc_time`
is interpreted as UTC and converted to `CLOCK_REALTIME` seconds with
zero nanoseconds.

Every failure in this step sends peinit to recovery: no openable RTC
device, a failed read, a value that is invalid or before the Unix epoch,
a failed `clock_settime`, and — deliberately — a failure to close the
descriptor after a successful read. Leaking a descriptor in PID 1 during
bootstrap is a symptom of something being badly wrong, not a detail to
swallow.

> [!NOTE]
> NTP corrects the clock properly later in the boot. This step only gets
> it into the right decade, so that early timestamps are not absurd and
> the timer subsystem has a wall clock to anchor to.

## 2.3.6 Step 6: Start registryd

peinit holds a compiled-in definition for registryd — the only compiled-in
service definition there is:

| Field | Value |
|---|---|
| ImagePath | `/sbin/registryd` |
| Arguments | `Machine=/var/state/loregd/Machine.hive`, `Users=/var/state/loregd/Users.hive` |
| Identity | `SYSTEM` |
| Readiness | Notify |
| ErrorControl | Critical |

The hive paths are peinit's choice, not registryd's: where the machine
registry lives is a boot-policy decision, and it is made here because
this is the one service start that cannot consult configuration.

peinit mints a SYSTEM token including registryd's per-service SID,
creates the cgroup tree, forks with the token installed, and execs
`/sbin/registryd` through the runtime StrataFS view. Two separate
timeouts bound the start, both 30 seconds: one on process setup, driven
synchronously because there is no event loop yet, and one on readiness.

registryd's `READY=1` means "accepting and serving registry requests",
not "the process is alive" — it does not signal until its storage
backend is open, its schema is validated and it can answer a read.

### 2.3.6.1 The schema-version guard

After readiness, peinit ensures the base registry structure exists and
then probes it. Ensuring comes first: peinit creates `Machine\System`,
`Machine\System\Services` and `Machine\System\Init` if they are absent
and stamps `Machine\System\Services\SchemaVersion` with the current
schema version, 1. Only then does it read the value back.

The read is what verifies registryd is genuinely serving. A value that
is present but not a `REG_DWORD`, or that is a `REG_DWORD` of the wrong
length, fails the probe. A key or value that is absent reads as zero and
passes — the structure was just created, so absence at this point means
the write did not take effect, and the failure that matters is the
provisioning failure, which is reported directly.

The consequence is that the guard is self-healing. An unprovisioned
first boot, or a registry cleared by the recovery tools, comes up with
an empty `Machine\System\Services\` and boots into a Phase 2 with no
services rather than into recovery.

### 2.3.6.2 Keeping registryd

When registryd passes readiness and the probe, peinit retains the
activation as an ordinary runtime service instance: its state, its
pidfd-tracked main process, its cgroup generation and paths, its output
pipe ownership, its notify generation, its job identity, and any cleanup
evidence. Ownership is not dropped at the Phase 2 boundary.

During Phase 2 the registry's own definition of `registryd`, if there is
one, is merged onto the retained activation. peinit does not create a
second inactive record and does not restart registryd because a
definition has appeared. If the registry definition is absent or
invalid, the ordinary graph validation rules apply.

If registryd fails to start, its readiness times out, or the probe
fails, peinit enters recovery. There is no Phase 2 without a registry.

## 2.3.7 Step 7: Autorun scripts

Between registryd starting and path provisioning, peinit runs every
non-directory entry in `/lcl/policy/autorun.d`, in sorted order, by
absolute path, with the working directory `/` and `PATH=/sbin:/bin`.
Each runs under peinit's own SYSTEM token.

The step is fail-open at every point: a missing directory, an unreadable
directory, a spawn failure and a non-zero exit are all console warnings
and none of them stops the boot. Its console output bypasses the quiet
policy (§2.6), because a script that ran this early and went wrong needs
to be visible.

## 2.3.8 Step 8: Provision boot-time paths

Covered in §2.4.

## 2.3.9 Step 9: Infrastructure setup

Three things, before Phase 2 begins:

1. **The control socket** at `/run/services/peinit/control.sock`, which
   serves every runtime command for the lifetime of the system.
2. **The JFS device**: peinit opens `/dev/jfs` and adds the descriptor
   to its event loop, enabling ad-hoc job submission once Phase 2 runs.
3. **Loopback**: peinit brings up `lo` over netlink, because services
   that bind `127.0.0.1` need it.

Control socket creation failing sends peinit to recovery — without it
there is no way to administer the system. The other two are warnings:
a JFS open failure, a JFS event-loop registration failure and a loopback
bring-up failure all let Phase 2 proceed.

## 2.3.10 Failure summary

| Failure | Response |
|---|---|
| Root writability probe fails | Recovery |
| A mount point cannot be created | Recovery |
| A peinit-owned filesystem fails to mount | Recovery |
| A mounted filesystem cannot be stamped with its descriptor | Recovery |
| `/proc/self/mountinfo` unreadable or unparseable | Recovery |
| `/proc`, `/sys` or `/dev` already mounted, or `EBUSY` | Tolerated as success |
| A device node in the policy list cannot be stamped | Warning; node keeps the inherited default |
| A device node in the policy list does not exist | Noted; boot continues |
| Random seed absent, oversized, empty, or unrestorable | Warning; boot continues |
| Machine ID read, generation, or write fails | Recovery |
| Machine ID absent, empty, or malformed | Regenerated; boot continues |
| Any RTC or clock failure | Recovery |
| registryd fails to start, or setup times out | Recovery |
| registryd readiness times out | Recovery |
| Base registry provisioning fails | Recovery |
| Schema-version probe returns a wrong type or length | Recovery |
| An autorun script is missing, unspawnable, or exits non-zero | Warning; boot continues |
| A provisioning entry is malformed | Warning; entry skipped |
| An optional provisioned path fails | Warning; boot continues |
| A required provisioned path fails | Recovery |
| Control socket creation fails | Recovery |
| JFS open or registration fails | Warning; boot continues |
| Loopback bring-up fails | Warning; boot continues |

---

# 2.4 Path Provisioning

_Peios / Advanced Peios / peinit / Boot_

> Filesystem objects belonging to no single service — how entries are declared, what descriptors they get, and what failure does.

Some filesystem objects belong to no single service. A directory two
packages both write into, a state file created before anything runs, a
path that has to exist with a particular Security Descriptor before the
first service that uses it starts — none of these has an owner in the
service model, and creating them from a service's pre-exec hook makes
their existence depend on start ordering.

Boot-time path provisioning is the registry-backed answer, and the
equivalent of the tmpfiles.d role elsewhere. peinit applies it after
registryd is serving and before any Phase 2 service is planned or
started.

## 2.4.1 Entries

Each child key under `Machine\System\Init\ProvisionedPaths\` is one
entry. Unknown values on an entry are ignored.

| Value | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `Kind` | string | yes | — | `directory` or `file`. |
| `Path` | string | yes | — | Absolute path to create or verify. |
| `Security` | binary | no | built-in | The Peios file Security Descriptor to apply. |
| `Required` | dword | no | 0 | If 1, failing this entry prevents Phase 2. |

For `Kind=directory` peinit ensures the path exists as a directory; for
`Kind=file` it ensures the path exists as a regular file. In either case
a path that exists with a different file type fails the entry. An
existing file is opened rather than created, so provisioning never
truncates one.

peinit does not create parent directories. The parent is checked, and has
to already be a directory. A package that needs a hierarchy declares
each directory explicitly, or depends on the package that owns the
parent — which keeps the ownership of every directory traceable to a
package rather than to whichever entry happened to run first.

## 2.4.2 Descriptors

When `Security` is present, peinit applies the supplied binary
descriptor. A malformed or rejected descriptor fails the entry.

When it is absent, peinit applies a built-in default:

```
O:SY G:SY D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FR;;;BU)
```

SYSTEM and Administrators get full control; ordinary users get
`FILE_GENERIC_READ`.

## 2.4.3 Failure

Entries with `Required=0` are fail-soft: peinit logs and continues.
Entries with `Required=1` are fail-closed: peinit logs and enters
recovery before Phase 2 starts.

An entry that is malformed — a missing or unrecognised `Kind`, a missing
or relative `Path`, a value of the wrong type — is logged as a warning
and skipped, regardless of `Required`. `Required` marks a path as
essential to boot; it does not make a broken entry more dangerous than a
missing one.

---

# 2.5 Phase 2

_Peios / Advanced Peios / peinit / Boot_

> The registry-driven phase — reading the definitions, building and validating the graph, starting it, and deciding the boot succeeded.

With registryd serving, peinit reads the service graph and boots the
system from it. Phase 2 is entirely registry-driven.

## 2.5.1 Reading the definitions

peinit reads every key under `Machine\System\Services\`. The reads are
bounded by LCS's request timeout: if registryd hangs mid-read, peinit
receives `ETIMEDOUT` and enters recovery.

Decoding is per key. A definition that fails to decode — an invalid
service name, a malformed trigger, an unclosed quote in a command, a
`registry:` check naming an uncacheable key, a duplicate known field, an
unrecognised value for an enumerated dword — fails that service, which
is marked Failed with cause `ValidationError`. The boot proceeds with
every other definition, and anything that depended on the failed service
fails in turn through the ordinary dependency propagation.

Only services carrying a `boot` trigger are root candidates. A service
with no triggers is demand-only and is not a root, though it can still
be pulled into the boot transaction as somebody's dependency. A service
with `Disabled=1` is excluded from the boot graph entirely, but its
definition is still loaded into the in-memory model so it can be started
by hand later.

## 2.5.2 Building and validating the graph

The boot graph is every boot-triggered root candidate plus the
transitive closure of their `Requires`, `BindsTo`, and existing
non-disabled `Wants` dependencies. `Requires` and `BindsTo` pull their
target in even if the target has no `boot` trigger. `Wants` targets come
in as best-effort members; missing or disabled ones are ignored. A
missing or disabled `Requires` or `BindsTo` target blocks the dependent
with cause `DependencyFailure`, and that blocking propagates.

peinit topologically sorts the graph and validates it before starting
anything. The rules are in §7.2; the outcomes that matter here are:

- **A cycle** fails every service in it. A cycle involving a Critical
  service downgrades the boot to Safe mode without rebooting.
- **An unresolvable conflict** fails both services. Same downgrade if
  either is Critical.
- **A missing `Requires` target** fails the dependent.
- **Warnings** — a `Readiness=Alive` service with dependents that
  require it — are logged and do not prevent boot.

## 2.5.3 Starting

peinit walks the graph and starts services, parallelising wherever the
graph allows, up to a configurable limit:

| Key | Default | Meaning |
|---|---|---|
| `Machine\System\Boot\MaxParallelStarts` | 10 | Services starting concurrently. |

An absent key uses the default. A value of zero, a type mismatch, or a
malformed payload is invalid boot configuration and sends peinit to
recovery — running the scheduler with an effective limit of zero would
hang the boot rather than fail it.

As each service reaches a dependent-satisfying state — Active for
Simple, Completed for Oneshot with or without `RemainAfterExit`, Skipped
for a service whose conditions did not hold — its dependents become
eligible and join the start queue. A Oneshot without `RemainAfterExit`
passes through Completed, releasing its dependents, and then goes
Inactive.

Dependents blocked on a `Requires` or `BindsTo` target wait for that
target to reach a satisfying state. Dependents blocked on a `Wants`
target wait only for it to reach *any* terminal state, satisfying or
not — which is what makes `Wants` ordering rather than dependency.

### 2.5.3.1 The typical order

Nothing below is hardcoded. It falls out of the dependency graph that
the standard role definitions produce, and an administrator who changes
a dependency gets a different order.

1. **eudev** — device management. SYSTEM, with `RequiredPrivileges`
   stripped to the minimum it needs. Starts before authd exists.
2. **lpsd** — the local identity database. Depends only on registryd.
3. **authd** — the identity authority. Depends on registryd and lpsd.
   Once it is ready, token minting is available.
4. **eventd** — logging and audit. Services started before it log into
   peinit's pre-eventd buffer.
5. **Networking** — the first service to receive a token from authd.
6. **Application services**, in dependency order.
7. **Login services**, last, so the system is operational before it
   accepts a session.

### 2.5.3.2 The bootstrap matrix

| Service | Phase | Identity | Token from | Readiness | ErrorControl |
|---|---|---|---|---|---|
| registryd | 1 | SYSTEM | minted by peinit | Notify | Critical |
| eudev | 2 | SYSTEM | minted by peinit, privileges stripped | Alive | Normal |
| lpsd | 2 | SYSTEM | minted by peinit | Notify | Critical |
| authd | 2 | SYSTEM | minted by peinit | Notify | Critical |
| eventd | 2 | SYSTEM | minted by peinit | Notify | Critical |
| networking | 2 | service token | authd | Notify | Normal |
| sshd | 2 | service token | authd | Alive | Normal |
| application services | 2 | service token | authd | per-service | Normal |

## 2.5.4 Deferred starts

A service whose trigger is `boot:settled` is not part of the boot plan
at all. It is not a root, it does not consume the parallel-start budget,
it is not counted towards boot success, and it cannot block or delay
anything. peinit starts it after the plan, once the boot has stopped
moving.

"Stopped moving" is precise: every service in the plan — those that were
started and those that were blocked — is in a state it will not leave
without help. Active, Completed, Failed, Skipped, Abandoned and Inactive
all count as settled. Starting, Reloading, Stopping and **Backoff** do
not, because a service between restart attempts is going to produce more
output.

A deadline bounds the wait:

| Key | Default | Meaning |
|---|---|---|
| `Machine\System\Boot\SettleTimeout` | 5 | Seconds before deferred services start regardless. |

The deadline is measured from the moment the plan was observed. An
absent key uses the default; a type mismatch or a bad length sends
peinit to recovery. Zero is legal and means "start on the next turn,
settled or not".

Whichever comes first — the set settling or the deadline expiring — the
deferred services start, once, each independently. A start that fails is
recorded and dropped: one refusing service does not stop the others and
does not affect the boot. The dispatch carries a flag saying whether the
deadline expired rather than the set settling, so a service that cares
whether the boot was still moving can be told.

> [!NOTE]
> The motivating case is a console login prompt being scribbled over by
> peinit's own progress messages. That is a scheduling preference, not a
> dependency — the prompt does not *require* anything to have started,
> it just wants the console to itself. Expressing it as a dependency
> would mean naming every service that might print, which changes every
> time the system does.

## 2.5.5 Boot success

A boot is successful once every Critical service has held a
dependent-satisfying state continuously for a grace period:

| Key | Default | Meaning |
|---|---|---|
| `Machine\System\Boot\BootSuccessGrace` | 30 | Seconds of held health before the boot counts. |

The criterion is *satisfying*, not Active. A Critical Oneshot reaches
Completed and never reaches Active, so a test for Active would make such
a service unable to ever mark a boot successful. Skipped counts too.

Success resets the boot attempt counter to zero (§2.7).

## 2.5.6 Failure summary

| Failure | Response |
|---|---|
| A service definition fails to decode | Recovery |
| A registry read times out | Recovery |
| Invalid `MaxParallelStarts` or `SettleTimeout` | Recovery |
| A dependency cycle | All services in it Failed; Safe mode if any is Critical (see below) |
| An unresolvable conflict | Both Failed; Safe mode if either is Critical (see below) |
| A missing `Requires` target | The dependent Failed |
| A Critical service fails during boot | Restart budget, then reboot |
| A non-Critical service fails | Failed; its `Requires` dependents fail; the rest continue |
| authd unavailable when a service needs a token | That service Failed |

The two Safe-mode rows carry a caveat. When the downgrade fires, peinit
rebuilds the graph in Safe mode and discards the Full-mode one, so the
services that caused it are **not** marked Failed — they are never
entered into the blocked set at all. What records them is the boot-level
downgrade finding described in [Boot modes](/peios/advanced-peios/peinit/boot/boot-modes.md).

---

# 2.6 Boot Modes

_Peios / Advanced Peios / peinit / Boot_

> Full, Safe and Recovery — the escalation path from normal operation to last-resort maintenance, and what causes each downgrade.

peinit boots in one of three modes, forming an escalation path from
normal operation to last-resort maintenance.

```
Full boot ---+-- success ----------------> counter reset, operational
             |
             +-- cycle w/ Critical ------> Safe mode (no reboot)
             |
             +-- conflict w/ Critical ---> Safe mode (no reboot)
             |
             +-- Critical failure -------> sync + reboot --+
                                                           |
Safe boot ---+-- success ----------------> counter reset,  |
             |   operational (reduced)                     |
             +-- Critical failure -------> sync + reboot --+
                                                           |
                             counter increments <----------+
                                        |
                             counter >= N ---------> Recovery mode
                             Phase 1 failure ------> Recovery mode
```

## 2.6.1 Full mode

The default. Every boot-triggered service starts in dependency order, as
§2.5 describes.

## 2.6.2 Safe mode

Safe mode starts a reduced set. Eligibility is a filter *within* the
boot-triggered set, not a replacement for it: a service with no `boot`
trigger does not auto-start in Safe mode whatever its `SafeMode` or
`ErrorControl` says, and remains demand-only. Within the boot-triggered
set, two categories are eligible:

- **Critical services** (`ErrorControl=Critical`) start. If one fails,
  the ordinary Critical failure path applies — restart budget, reboot,
  counter increment, eventually recovery.
- **`SafeMode=1` services** are attempted best-effort. If one fails,
  Safe mode continues without it.

`ErrorControl=Critical` implies `SafeMode`, so a Critical service does
not have to declare both.

### 2.6.2.1 What caused the downgrade

The rebuild discards the Full-mode graph, so the services that forced
Safe mode are never entered into the blocked set and are never marked
Failed. That is deliberate: Safe mode was never going to start them, and
a Failed state would say something about their own health that is not
true. `status` should keep meaning "this service is broken".

The reason is therefore recorded at **boot level** rather than per
service. Every finding that forced the downgrade — each critical cycle,
each critical boot conflict — is written to the console and emitted as a
`boot.safe_mode_downgrade` KMES event naming the services involved.

All of them are reported, not just the first. A machine can be downgraded
by a cycle *and* a conflict at once, and an operator who fixed only the
one they were shown would reboot straight back into Safe mode.

peinit rebuilds the dependency graph from scratch using only the
eligible services. Dependencies on excluded services are dropped: if A
depends on non-Critical B and B is excluded, A's dependency on B does
not exist in the Safe mode graph. This is what makes Safe mode useful —
it is a graph in which the broken parts of the configuration are simply
not present, rather than a graph in which they are present and failing.

A successful Safe boot resets the boot attempt counter.

> [!NOTE]
> Safe mode is purely a boot-sequencing concern. Once booted, an
> administrator starts anything by hand exactly as in Full mode. It is
> for a system whose configuration is broken but whose TCB is healthy.

### 2.6.2.2 Entry

- **A cycle involving a Critical service at boot.** Graph validation
  detects it and peinit downgrades in place, without rebooting — the
  cycle is a configuration error, and rebooting would find it again.
- **An unresolvable conflict involving a Critical service at boot.**
  Same reasoning.
- **`peios.safemode=1`** on the kernel command line.

Safe mode is not entered because a Critical service crashed at runtime.
That follows the ordinary path: restart budget, reboot, counter
increment, recovery.

## 2.6.3 Console output

`peios.quiet=N` bounds what peinit writes to the console:

| Value | Behaviour |
|---|---|
| `0` | Write unconditionally. |
| `1` | Do not write to a terminal held as the controlling terminal of a running service, except to announce loss of the system. This is the default. |
| `2` | Additionally drop ordinary progress everywhere, while still emitting errors. |

The two rules are independent, and an error is never less visible at `2`
than at `1`. A terminal is matched by device rather than by path, since
`/dev/console` and `/dev/ttyS<n>` can name the same device; where the
device cannot be determined, peinit falls back conservatively and treats
the terminal as held. Suppressed messages are discarded rather than
buffered.

The autorun step (§2.3) bypasses the policy: a script that ran that
early and went wrong is worth interrupting a login prompt for.

## 2.6.4 Kernel command line

| Parameter | Effect |
|---|---|
| `peios.safemode=1` | Force Safe mode. |
| `peios.recovery=1` | Force recovery mode regardless of the counter. |
| `peios.bootattempts=N` | Set the recovery threshold; `0` disables the check. |
| `peios.quiet=N` | Console verbosity, as above. |
| `peios.notifysocket=PATH` | Override the notification socket path. |

A malformed value is ignored in favour of the default rather than
failing the boot. Nothing exists this early to report a diagnostic to.

---

# 2.7 The Boot Attempt Counter

_Peios / Advanced Peios / peinit / Boot_

> The on-disk counter that turns a repeated failure into an escalation — its cycle, and the threshold that reaches Recovery.

The counter is what turns a repeated failure into an escalation. peinit
keeps it at `/.peinit/boot-attempts`, as a plain decimal integer in a
file on the root filesystem — deliberately not in the registry, because
the registry may be the reason the boot is failing.

## 2.7.1 The cycle

peinit reads the counter at startup, before selecting a boot mode. The
recovery threshold is evaluated against this **pre-increment** value, so
a default threshold of 3 admits exactly three boot attempts before
recovery.

- **Absent file:** treated as 0.
- **Unreadable, empty, non-decimal, carrying trailing non-whitespace
  data, or overflowing the counter representation:** recovery. A counter
  that cannot be read cannot be trusted to escalate.

peinit increments once per boot, after the root is known writable and
before Phase 2 begins. Incrementing before the root is known writable
would silently lose the increment on a read-only root and defeat
escalation entirely.

The increment happens after the mount, seed, machine ID and clock steps
rather than immediately after the writability probe, because reading the
kernel command line requires `/proc`. One consequence is that a recovery
entered from a mount failure, a machine ID failure, or an unreadable
command line does not advance the counter.

A write failure — a full disk, say — is treated as a counter of 0 and
the boot continues. A failure to record an attempt is not itself a
reason to escalate.

The counter resets to 0 on a successful Full or Safe boot, after the
grace period.

## 2.7.2 Recovery threshold

The threshold is `peios.bootattempts=N` on the kernel command line,
defaulting to 3. It is a command-line value rather than a registry one
because the check runs in Phase 1, before registryd is serving.

`peios.bootattempts=0` disables the check entirely — the escape hatch
for a system whose counter is itself the fault.

`peios.recovery=1` forces recovery without consulting the counter, but
does not suppress the increment.

> [!NOTE]
> The counter catches boots where peinit runs but the system never
> reaches health — a crash-looping Critical service being the usual
> case. It deliberately does not try to catch a peinit too broken to
> reach its own increment. Recovery mode is itself a peinit mode, so a
> peinit that cannot start cannot deliver recovery either, and a higher
> count could not help. That failure belongs to binary integrity, not to
> the counter.

---

# 2.8 Recovery Mode

_Peios / Advanced Peios / peinit / Boot_

> The last resort — no TCB guarantee, an administrator shell, offline registry access, and what remote recovery there is.

Recovery mode is the last resort. There is no TCB guarantee and no
degraded boot to speak of — it is a maintenance environment that hands
the administrator an unrestricted SYSTEM shell on the console.

## 2.8.1 Entry

- The boot attempt counter reaching the threshold (§2.7).
- `peios.recovery=1` on the kernel command line.
- Any of the Phase 1 failures listed in §2.3, most importantly a
  registryd that will not start or will not serve.
- A Phase 2 registry read that fails or times out, or invalid boot
  configuration.
- A required provisioned path that cannot be created or secured.

## 2.8.2 What peinit does

peinit records the reason as a KMES audit event, then:

1. Completes Phase 1 steps 1–5 if they have not been reached yet.
2. Ensures the base registry structure exists, so the shell sees a
   normal layout even on a system that has never been provisioned.
3. Attempts to start registryd. A failure here is ignored — recovery
   delivers a shell whatever registryd's state.
4. Skips all Phase 2 services.
5. Starts a shell on `/dev/console` from a compiled-in definition, with
   no registry dependency: `/bin/recsh` if it is present and
   executable, otherwise `/bin/sh`, running as SYSTEM with a fixed
   environment of `PATH=/sbin:/bin`, `TERM=linux` and `HOME=/`. peinit
   does not care where either binary comes from.
6. Logs the failure reason to the console.

If the shell exits, peinit respawns it. Recovery never exits to an
unmanaged PID 1.

If neither `/bin/recsh` nor `/bin/sh` can be exec'd, peinit cannot
deliver a shell at all. It logs the reason to the console, syncs, and
halts — PID 1 exiting would panic the kernel. A missing shell is a
binary-integrity failure and sits outside the boot-attempt machinery's
remit.

The shell receives `/dev/console` duplicated onto its standard streams,
but peinit does not call `setsid()` or acquire a controlling terminal
for it. The shell is not a session leader, so job control is not
available in the recovery shell.

Whether the earlier steps are re-run depends on where the failure came
from. A recovery entered from a Phase 2 or runtime failure has already
completed Phase 1 and has registryd running. A recovery entered from a
Phase 1 failure — a mount that would not mount, an RTC that would not
read, a control socket that would not bind — skips steps 1 and 3 above:
it neither completes the remaining Phase 1 steps nor attempts registryd.

> [!NOTE]
> Recovery mode is not a degraded boot; it is a maintenance environment.
> There are no security protections beyond what the kernel provides. The
> administrator has a SYSTEM shell and the corresponding responsibility.

## 2.8.3 Offline registry access

If registryd is what caused the recovery, the administrator needs tools
that work without it. Three paths exist:

| Path | What it does |
|---|---|
| `loregd --inspector` | Reads the storage database directly, bypassing LCS, for diagnosis. |
| `loregd --recover-from-backup` | Restores from the automatic backup taken on every registryd startup. |
| `loregd --dangerously-clear-database` | Wipes the registry. Role definitions are the source of truth for service configuration, so a cleared registry is recoverable. |

These name loregd rather than registryd because in recovery the
administrator is interacting with the storage implementation, not with
the registry abstraction. It is the one context where that distinction
is visible.

> [!NOTE]
> `/bin/recsh` exists so a system can ship a purpose-built recovery
> shell — one that bundles the offline registry tools, presents
> guidance, or curates a command set — without making it mandatory.
> `/bin/sh` is the floor where it is absent. peinit treats both as
> opaque executables.

## 2.8.4 Remote recovery

Recovery requires console access: physical, IPMI or serial. Two
post-v1 features address headless servers — rolling the registry back to
a last-known-good state from the recovery shell, and an emergency sshd
started without registry involvement.

---

# 3.1 Services

_Peios / Advanced Peios / peinit / The Service Model_

> The primary unit of management — names, the two service types, and how a forking daemon differs from a simple one.

A service is the primary unit of management: a definition in the
registry, a runtime state, a Security Descriptor, and at most one
running main process. Definitions live under
`Machine\System\Services\<name>`, where the key name *is* the service
name. peinit reads them at Phase 2 boot and on an explicit
reload-config.

## 3.1.1 Names

A service name is 1 to 128 bytes drawn from `[A-Za-z0-9._-]`. Any other
byte makes the name invalid.

Two exclusions are deliberate. `/` is out because names map directly
onto cgroup identifiers (§5.1) and onto registry key names, and a name
containing a separator would mean something different in each. `:` is
reserved for peinit's own synthetic naming.

## 3.1.2 The two types

### 3.1.2.1 Simple

A long-running daemon. peinit forks, installs a token, and execs the
binary; the process *is* the service. When it exits, the service has
stopped. This is the default and covers nearly everything — registryd,
authd, sshd, application services.

Readiness comes from the `Readiness` field. `Notify`, the default,
waits for `READY=1`. `Alive` treats the process as ready the moment it
exists.

The service goes Active on readiness and stays Active until the process
exits or something stops it.

### 3.1.2.2 Oneshot

A run-to-completion task: database initialisation, a schema migration, a
directory that has to exist. peinit forks, installs a token, execs, and
waits for the exit.

`Readiness` is ignored. A Oneshot's readiness is always "it exited
successfully", because `READY=1` is meaningless from a process whose job
is to finish. Success means exit code 0, or any code listed in
`SuccessExitCodes`.

The differences from Simple are:

- A successful exit goes to Completed. With `RemainAfterExit=1` it stays
  there; without, it passes through Completed to release dependents and
  then goes Inactive.
- A non-zero exit goes to Failed.
- `ExecStartPost` runs after the successful exit rather than after a
  readiness signal, and does not run at all if the Oneshot failed.
- `StartTimeout` covers the entire execution, from the first pre-hook to
  the process exiting.

`RemainAfterExit` matters when the Completed state itself is the useful
information — so a status query shows a migration as finished rather
than as inactive.

## 3.1.3 Forking daemons

peinit does not support them. A service that double-forks to daemonise
itself is working around a problem that does not exist when the service
manager tracks the process it spawned, and peinit tracks its child
through a pidfd obtained at fork. There is no `MAINPID=`, and no way to
point supervision at a different process.

A legacy binary that insists on double-forking is wrapped by whoever
packages it — a script with a `--no-daemon` flag, typically. That is a
packaging concern.

---

# 3.2 The Definition Schema

_Peios / Advanced Peios / peinit / The Service Model_

> Every field a service definition can carry, with its registry type and default, and why decoding is all-or-nothing.

Every field a service definition can carry, with its registry type and
its default. The semantics of each are in the section named alongside.

Value names are matched case-insensitively, so `ImagePath` and
`imagepath` are the same field — and therefore a definition carrying
both is a duplicate, not two fields.

| Field | Type | Default | Meaning |
|---|---|---|---|
| ImagePath | string | *required* | Absolute path to the service binary. |
| Arguments | multi_string | — | Arguments passed to the binary. |
| Type | dword | 0 (Simple) | 0 Simple, 1 Oneshot. §3.1 |
| Triggers | multi_string | — | When the service starts automatically. §3.4 |
| Disabled | dword | 0 | If 1, no trigger activates the service. |
| SafeMode | dword | 0 | If 1, attempt this service in Safe mode. Implied by `ErrorControl=Critical`. §2.6 |
| Identity | string | LocalService | Principal for the service token. §4.1 |
| RequiredPrivileges | multi_string | — | Privileges to keep; all others are removed. §4.5 |
| Requires | multi_string | — | Hard dependencies. §7.1 |
| Wants | multi_string | — | Soft dependencies. §7.1 |
| BindsTo | multi_string | — | Runtime coupling. §7.1 |
| Conflicts | multi_string | — | Mutual exclusion. §7.1 |
| OnFailure | string | — | Service to start when this one fails. §6.3 |
| ErrorControl | dword | 0 (Normal) | 0 Normal, 1 Critical. |
| RemainAfterExit | dword | 0 | Oneshot only: stay Completed after a successful exit. |
| SuccessExitCodes | multi_string | — | Non-zero exit codes treated as success. |
| ExecStartPre | multi_string | — | Commands run before the main binary, sequentially. §5.3 |
| ExecStartPost | multi_string | — | Commands run after readiness or successful exit. §5.3 |
| HookIdentity | string | — | Principal for the hook processes. Falls back to `Identity`. §4.1 |
| ExecReload | string | — | Reload command, or `signal:<NAME>`. Absent means SIGHUP. §6.5 |
| PreStartCheckTimeout | dword | 5 | Seconds before a filesystem check helper is killed. §3.5 |
| StartTimeout | dword | 30 | Seconds for the entire start sequence. §5.3 |
| StopTimeout | dword | 10 | Seconds after SIGTERM before SIGKILL. |
| WatchdogTimeout | dword | 0 | Seconds between expected `WATCHDOG=1` pings; 0 disables. §6.6 |
| HealthCheck | string | — | Command run periodically. Exit 0 is healthy. §5.6 |
| HealthCheckInterval | dword | 30 | Seconds between health checks. |
| HealthCheckTimeout | dword | 5 | Seconds before a health check is killed and counted failed. |
| HealthCheckRetries | dword | 3 | Consecutive failures before the service is unhealthy. |
| RestartPolicy | dword | 1 (OnFailure) | 0 Never, 1 OnFailure, 2 Always. §6.4 |
| RestartMaxRetries | dword | 5 | Consecutive restarts before Failed. §6.4 |
| RestartWindow | dword | 120 | Seconds of sustained health that reset the restart counter. |
| RestartDelay | dword | 1 | Seconds before a restart; doubles each consecutive failure, capped at 60. |
| Readiness | dword | 0 (Notify) | 0 Notify, 1 Alive. Ignored for Oneshot. |
| NotifyAccess | dword | 0 (Main) | Who may send notifications. Main is the only mode. §10.5 |
| FdStoreMax | dword | 0 | Maximum descriptors held for the service; 0 disables the store. §10.6 |
| TimerPersistent | dword | 1 | Catch up a missed timer run after a reboot. §9.3 |
| TimerJitter | dword | 0 | Maximum random delay added to each firing. §9.4 |
| Environment | multi_string | — | `KEY=VALUE` pairs added to the environment. §5.5 |
| WorkingDirectory | string | `/` | Working directory for the process. |
| TTYPath | string | — | Terminal to attach as the standard streams and controlling terminal. §5.4 |
| RuntimeDirectories | multi_string | — | Private directories under `/run`, created before the main process. |
| LimitNOFILE | dword | — | `RLIMIT_NOFILE`. |
| LimitCORE | dword | — | `RLIMIT_CORE`, in bytes. |
| Conditions | multi_string | — | Start-time conditions; failure skips the service. §3.5 |
| Asserts | multi_string | — | Start-time assertions; failure fails the service. §3.5 |
| DisplayName | string | — | Human-readable name for status display. |
| Description | string | — | What the service does. |
| ServiceSecurity | binary | inherit | Descriptor controlling runtime operations on the service. §4.6 |

## 3.2.1 Registry types

| Schema type | Registry type |
|---|---|
| string | `REG_SZ`, UTF-8 |
| multi_string | `REG_MULTI_SZ`, an ordered list |
| dword | `REG_DWORD`, 32-bit unsigned |
| binary | `REG_BINARY` |

A value whose registry type does not match the field's is a decode
error, as is a dword carrying a value outside an enumerated field's
range.

## 3.2.2 Schema version and forward compatibility

`Machine\System\Services\SchemaVersion` is a dword, currently 1. peinit
creates it if it is absent (§2.3).

Unknown values on a service key are ignored, which is what lets the
schema grow additively: a definition written for a newer peinit still
loads on an older one, minus the fields it does not understand. A newer
schema version does not prevent boot.

Known fields are the opposite. A known field appearing more than once in
a collected definition is a decode error rather than a last-one-wins,
because a definition that says two different things about the same field
has no defensible reading. Since names match case-insensitively, this
catches `ImagePath` and `imagepath` in the same key.

## 3.2.3 What a decode failure costs

A definition that fails to decode fails that one service, and the answer
differs by caller.

At boot the key is marked Failed with cause `ValidationError` and the
boot proceeds with every other definition. Anything that depended on the
failed service fails in turn through the ordinary dependency propagation
(§7.4), so the cost is bounded by what actually needed it.

On reload-config the whole read is rejected and the previous generation
stays in place (§10.4). That is not an inconsistency: a reload is atomic
and has a working configuration to fall back to, where a boot has none.
Refusing everything is the safe answer only when there is something to
keep.

---

# 3.3 Field Formats

_Peios / Advanced Peios / peinit / The Service Model_

> How a field's value must be written, as distinct from what it means — strings, identity, privileges, directories and environment.

Rules that apply to how a field's value is written, as distinct from
what it means.

## 3.3.1 Strings

A string field that is present is non-empty, unless this section says
otherwise for that field. Three fields treat the empty string as
absence:

- `Identity` — empty is the same as absent, and defaults to
  `LocalService`.
- `HookIdentity` — empty is the same as absent, and falls back to the
  service's `Identity`.
- `DisplayName` and `Description` — empty is the same as absent.

`WorkingDirectory`, when present, is a non-empty absolute path. Whether
it exists, is a directory, and is reachable is checked when the service
starts, not when the definition is read.

`TTYPath` is checked for emptiness before absoluteness: an empty value
means no terminal, and a non-empty relative path is rejected.

## 3.3.2 Identity and HookIdentity

Either a well-known principal name — `SYSTEM`, `LocalService`,
`NetworkService` — matched case-insensitively and canonicalised, or a
literal SID string such as `S-1-5-18`. Anything else is passed to authd
verbatim to resolve (§4.3).

## 3.3.3 RequiredPrivileges

Privilege names, matched **case-sensitively** against the published
privilege table. A name that does not match exactly fails token
materialisation and therefore the service start, so `SeTCBPrivilege`
does not start a service that `SeTcbPrivilege` would.

## 3.3.4 RuntimeDirectories

Each entry names one directory directly under `/run`. Entries are
non-empty relative names; an entry equal to `.` or `..` is rejected, as
is any entry containing `/`, `\`, a NUL, or a control character. A dot
inside a name is fine — `app.sock.d` is a valid entry.

For an entry `foo`, peinit creates `/run/foo` immediately before
launching the service's main process, with a descriptor granting full
access to SYSTEM, Administrators, and the service's own SID. Hook
processes inherit the service's environment and identity rules but do
not cause provisioning — the directories belong to the main start.

If creation or descriptor assignment fails, the start fails with
`ParentSetupFailure`.

peinit does not remove runtime directories when a service stops. `/run`
is a boot-scoped tmpfs and the next boot clears it.

## 3.3.5 Environment

Each entry is `KEY=VALUE`, with a non-empty key containing no NUL. An
entry that does not split into that shape is a decode error.

## 3.3.6 SuccessExitCodes

Each entry is a decimal integer from 0 to 255 — a process exit code.
Signal names and ranges are not accepted. Code 0 is always success and
does not need listing. Duplicates are collapsed.

## 3.3.7 Dependency and handler names

Entries in `Requires`, `Wants`, `BindsTo` and `Conflicts`, and the
value of `OnFailure`, are validated as service names when the definition
is read. A dependency naming something outside `[A-Za-z0-9._-]` is a
decode error rather than an unresolved dependency discovered later —
the difference being that a typo containing an illegal character is
caught immediately, while a typo that is still a legal name is caught at
graph validation as a missing target.

## 3.3.8 Timeouts and intervals

Every timeout and interval in the schema is in whole seconds unless the
field says otherwise. The two `sd_notify` fields that carry durations,
`WATCHDOG_USEC` and `EXTEND_TIMEOUT_USEC`, are in microseconds because
that is what the protocol specifies.

## 3.3.9 Identifiers peinit generates

Job identifiers, operation identifiers, and every other GUID peinit
mints are UUIDv7. UUIDv7 is time-ordered, so identifiers sort by
creation time — which is what keeps eventd's time-range and recency
queries over jobs and operations cheap.

---

# 3.4 Triggers

_Peios / Advanced Peios / peinit / The Service Model_

> What makes a service start by itself — the trigger kinds, how arity is enforced, and how the set is extended.

A trigger says when a service starts by itself. Triggers are independent
of service type: a Simple service can have a timer, and a Oneshot can
start at boot.

`Triggers` is a multi_string, each entry either `type` or
`type:argument`.

| Trigger | Form | Meaning |
|---|---|---|
| Boot | `boot` | Start during the Phase 2 boot sequence. |
| Deferred boot | `boot:settled` | Start once the boot set has settled, or the deadline expires. §2.5 |
| Timer | `timer:<schedule>` | Start on a schedule. §9.1 |

A service with no triggers is demand-only: it starts only when something
asks for it, whether an administrator, a dependency, or an `OnFailure`
handler.

Multiple triggers of the same type are allowed. A service with
`["timer:*-*-* 02:00:00", "timer:*-*-* 14:00:00"]` runs at 2am and 2pm,
and each trigger is independent — its own timerfd, its own next-firing
computation, its own history.

## 3.4.1 Arity is enforced

`boot` takes no argument. `boot:<something>` accepts only the
sub-triggers in the table above, and any other value is malformed rather
than an unknown trigger type to be ignored. `timer` with no schedule is
malformed, as is `timer:` with an empty one.

That strictness is the point. Unknown *values* on a service key are
ignored for forward compatibility, and if unknown trigger types were
ignored too, `boot:setled` would be silently accepted and the service
would simply never start. Instead it fails to decode, loudly.

## 3.4.2 Disabled

`Disabled=1` suppresses automatic activation and nothing else. No
trigger fires: not `boot`, not `boot:settled`, not a timer, and not any
trigger type added later. A disabled service's timers are neither armed
nor serviced.

The definition is still loaded into the in-memory model, and an explicit
start still works. To stop a service being started at all, deny
`SERVICE_START` in its `ServiceSecurity` descriptor — that is an access
control question, not a trigger question, and answering it with the
`Disabled` flag would mean a flag that anyone who can write the key can
clear.

Enabling and disabling are registry writes performed by administrative
tools, not peinit commands. peinit picks the change up through a
registry change notification, or on the next reload-config.

## 3.4.3 Extensibility

The `type:argument` shape is designed to grow. Path, device and event
triggers slot into the same array with no schema change, because a
trigger is a string in a list rather than a field of its own.

---

# 3.5 Conditions and Asserts

_Peios / Advanced Peios / peinit / The Service Model_

> Two start-time checks in the same form over the same four check types, differing only in what a failure means.

Both are start-time checks in the same `type:argument` form, over the
same four check types. What differs is the consequence of a failure.

| Check | Form | Passes when |
|---|---|---|
| path | `path:<path>` | The path exists, of any type. |
| file | `file:<path>` | A regular file exists there. |
| directory | `directory:<path>` | A directory exists there. |
| registry | `registry:<key>` | The registry key exists. |

**Conditions** describe when a service *applies*. A failed condition
skips the service: it transitions to Skipped, which satisfies its
dependents, because a service that does not apply has succeeded by not
needing to run.

**Asserts** describe what a service *needs*. A failed assert fails the
service, with cause `AssertionError` — the service was expected to run
and a precondition it depends on is missing.

All entries of a kind are AND'd. Conditions are evaluated first; asserts
only if every condition passed. Both are evaluated before dependency
resolution and before any pre-exec hook, and an entry with an empty
argument or an unrecognised type is a decode error.

## 3.5.1 Evaluation without blocking

peinit is single-threaded PID 1 and its event loop cannot block while a
check runs. Two constraints follow, and they are why the check types
behave differently from one another.

### 3.5.1.1 Registry checks are cache-only

A `registry:` check is evaluated against the in-memory model, never by a
live registry read. It can therefore only name a key peinit already
caches — under `Machine\System\Services\` or `Machine\System\Init\`.
Naming any other key is a decode error, caught at load rather than at
start.

Within that, resolution is narrower than the load-time check suggests.
A `registry:Machine\System\Services\<name>` check is true when that
service exists in the model, which makes it a subkey-existence test
rather than a general key-existence test.

### 3.5.1.2 Filesystem checks run in a helper

`stat()` can block uninterruptibly on hung I/O — a dead NFS mount, a
failing disk controller — so peinit does not call it from the event
loop. It forks a short-lived helper into a dedicated `checks/` cgroup
under the service's tree, using the same `clone3` path as any other
child. The helper stats the paths and reports over a non-blocking pipe;
peinit waits on the pipe and the helper's pidfd through epoll, and never
blocks.

`PreStartCheckTimeout`, default 5 seconds, bounds the helper. A check
that does not report in time is treated as **not satisfied** — the
fail-safe direction, so a condition skips the service and an assert
fails it. peinit then SIGKILLs the helper's cgroup and unregisters the
result descriptor, and the event loop is never held up by a hung check.

## 3.5.2 When results are computed

Checks are evaluated once, before the service's dependencies start, and
the result is cached for the rest of that activation. A service that
waits a long time for a dependency starts on the answer that was true
when the wait began, not on a fresh one.

---

# 3.6 Command Strings

_Peios / Advanced Peios / peinit / The Service Model_

> The four fields holding executable commands, how all four are parsed, how the executable is located, and ExecReload signals.

Four fields hold executable commands: `ExecStartPre`, `ExecStartPost`,
the command form of `ExecReload`, and `HealthCheck`. All four are parsed
the same way.

## 3.6.1 Parsing

The string is split on whitespace into an argv, with double quotes
grouping. There is no shell — no expansion, no substitution, no
globbing, and peinit never invokes one. A command that needs shell
features is wrapped in a script.

Whitespace means exactly six characters: space, horizontal tab, line
feed, carriage return, form feed and vertical tab. Every other Unicode
whitespace code point is an ordinary argument character, which keeps the
split independent of the Unicode version peinit was built against.

Double quotes group text into one argv element and are not retained in
the result. Grouping may happen inside an argument, so
`--name="hello world"` becomes the single entry `--name=hello world`.
An empty quoted string is preserved as an empty argv entry.

Backslash has no escape semantics and is copied literally. A single
quote is an ordinary character.

An empty or whitespace-only command is invalid, and so is an unclosed
double quote.

## 3.6.2 The executable

For all four fields, argv[0] is the executable path and begins with `/`.
Relative names, empty names and PATH-searched execution are decode
errors. Everything after argv[0] is an opaque string and is not path
validated.

## 3.6.3 ExecReload signals

`ExecReload` may instead name a signal, as `signal:<NAME>`. The name is
an exact canonical Linux signal name. Numeric values, realtime signal
expressions, aliases, lowercase spellings and names with surrounding
whitespace are all invalid.

`SIGKILL` and `SIGSTOP` are invalid for reload, because a service cannot
handle either as a request to re-read its configuration.

The accepted set is the standard non-realtime Linux signals other than
those two:

`SIGHUP`, `SIGINT`, `SIGQUIT`, `SIGILL`, `SIGTRAP`, `SIGABRT`,
`SIGBUS`, `SIGFPE`, `SIGUSR1`, `SIGSEGV`, `SIGUSR2`, `SIGPIPE`,
`SIGALRM`, `SIGTERM`, `SIGSTKFLT`, `SIGCHLD`, `SIGCONT`, `SIGTSTP`,
`SIGTTIN`, `SIGTTOU`, `SIGURG`, `SIGXCPU`, `SIGXFSZ`, `SIGVTALRM`,
`SIGPROF`, `SIGWINCH`, `SIGIO`, `SIGPWR`, `SIGSYS`.

Names are validated when the definition is read and again before
delivery, and mapped to host signal numbers at that point. `ExecReload`
absent means SIGHUP.

---

# 3.7 Configuration Generations

_Peios / Advanced Peios / peinit / The Service Model_

> peinit operates on snapshots rather than a live registry view — the boot and activation generations, field mutability, and pinning.

peinit operates on snapshots, not on a live view of the registry. Two
generation concepts govern when a registry change takes effect.

## 3.7.1 The in-memory model

peinit maintains a full in-memory model of every service definition. The
registry is read synchronously exactly twice: during Phase 2 boot,
before meaningful supervision has started, and during a reload-config,
which an administrator initiated and which is bounded. At all other
times peinit works from the model.

> [!NOTE]
> The model exists because the event loop cannot block on a userspace
> service during normal supervision. In single-threaded PID 1
> a blocking syscall blocks everything — child reaping, watchdog expiry,
> shutdown signals, every other event stops while the syscall is stuck.
> Reading the registry means waiting on registryd, and registryd is a
> service peinit supervises.

Change notifications arrive as events on a pollable descriptor. peinit
subscribes to `Machine\System\Services\` and `Machine\System\Init\` at
boot, using the LCS watch mechanism — a persistent subscription that
delivers change events on a key descriptor, with an OVERFLOW event when
the kernel-side queue is exceeded (Peios Kernel TRM §5).

Any drained watch event triggers a full reload of the configuration,
not a targeted re-read of the changed key. That covers the OVERFLOW case
by construction, and it is why an administrator writing one value causes
every definition to be re-read.

## 3.7.2 The boot generation

At the start of Phase 2, peinit reads all definitions, builds the
dependency graph and validates it. The plan and the graph are fixed at
that point, and the boot executes against them.

The watches are armed as the event loop starts, which is after the plan
is fixed but while boot-plan services are still starting. A registry
write during that window — from an install script, a post-hook, a
package transaction — triggers a reload like any other. Services that
are already running keep their pinned definition; a boot-plan service
that has not started yet picks up the new one.

## 3.7.3 The activation generation

When peinit starts a service, it snapshots that service's definition.
The snapshot governs the whole start lifecycle: pre-exec hooks, the
token request, the readiness timeout, the initial health checks. A field
changed while the service is Starting does not take effect until the
next start.

A service in Inactive or Failed has no activation snapshot, so starting
it uses the current model. That gives the expected behaviour for the
common edits:

- A new service entry is available once the change notification is
  processed.
- A timer change on an inactive service takes effect at the next
  trigger evaluation.
- A dependency change takes effect on the next start for an inactive
  service, and on the next restart for an active one.

## 3.7.4 Field mutability

Which class a field falls into depends on when its value is consumed.

### 3.7.4.1 Pinned to the running definition

A change takes effect only when the service is restarted:

`ImagePath`, `Type`, `Identity`, `RequiredPrivileges`, `ErrorControl`,
`RemainAfterExit`, `Triggers`, `Disabled`.

`Triggers` and `Disabled` are pinned only while the service is running.
On a service that is not running, both take effect as soon as the
notification is processed — which is what arms a timer added to an
inactive service.

### 3.7.4.2 Applied on the next start

A change takes effect at the next start or explicit graph reload, not
while services are running:

`Requires`, `Wants`, `BindsTo`, `Conflicts`, `OnFailure`, `Conditions`,
`Asserts`.

### 3.7.4.3 Reloaded at runtime

A change takes effect at the next relevant event, with no restart:

`Arguments`, `SuccessExitCodes`, every timeout and retry value
(`StartTimeout`, `StopTimeout`, `WatchdogTimeout`, `RestartDelay`,
`RestartMaxRetries`, `RestartWindow`, `PreStartCheckTimeout`),
`HealthCheck` and its three parameters, `RestartPolicy`, `Environment`,
`WorkingDirectory`, `ExecStartPre`, `ExecStartPost`, `ExecReload`,
`HookIdentity`, `Readiness`, `NotifyAccess`, `LimitNOFILE`,
`LimitCORE`, `FdStoreMax`, `TTYPath`, `RuntimeDirectories`,
`TimerPersistent`, `TimerJitter`, `SafeMode`, `DisplayName`,
`Description`, and `ServiceSecurity`.

`ServiceSecurity` is the one whose reload is immediately observable:
a change takes effect on the very next control request against that
service (§4.6).

---

# 3.8 Service Removal

_Peios / Advanced Peios / peinit / The Service Model_

> What happens when a definition disappears from the registry, depending on whether the service is running.

When a definition disappears from `Machine\System\Services\`, peinit
learns of it through the ordinary change-notification path. What happens
next depends on whether anything is running.

## 3.8.1 Not running

An entry in Inactive, Failed, Completed, Skipped or Abandoned is
discarded immediately. There is no process to consider.

## 3.8.2 Running

An entry in Active, Starting, Reloading, Backoff or Stopping is **not**
killed. The running process is a job, and a job's lifecycle is
independent of the definition that produced it — removing a definition
stops future management, it does not terminate work in progress.

peinit marks the entry **definition-removed** and keeps the cached
definition, solely to go on supervising the instance it already has.
When that instance exits it is not restarted — `RestartPolicy` is moot,
because there is nothing left to restart from — and peinit then discards
the entry.

While an entry is definition-removed:

- it keeps satisfying its dependents for as long as the instance is
  alive, because it is still running;
- `stop` is accepted, so an administrator can drain it cleanly, using
  the cached `StopTimeout`;
- `start`, `restart` and `reload` are rejected with `UNKNOWN_SERVICE` —
  there is no definition to work from;
- `status` reports the current runtime state with `definition_removed`
  set, so the draining instance is visible rather than silent.

`definition-removed` is a flag on the existing runtime state, not a
state of its own. The state machine (§6.1) and the command × state
matrix (§10.3) are unchanged by it.

Once the instance exits or is stopped, the entry — including anything in
its fd store (§10.6) — is discarded. A dependent that `Requires` the
removed service keeps being satisfied while the instance runs; after the
entry is discarded, the dependent's next start sees an unresolved
dependency and takes the ordinary validation path.

---

# 4.1 Token Materialisation

_Peios / Advanced Peios / peinit / Service Identity_

> Every service process runs with a KACS token — where peinit obtains or creates it, and where it is installed.

Every service process runs with a KACS token that determines its
identity and its access rights. peinit obtains or creates that token and
installs it on the child before exec. It never shares its own token —
even an `Identity=SYSTEM` service receives a separately materialised
token of its own.

Which route the token comes from depends on the `Identity` field:

| Identity | Source | Mechanism |
|---|---|---|
| `SYSTEM` | Minted by peinit from its own identity | `kacs_create_token`. §4.2 |
| Anything else | authd | The token request flow. §4.3 |
| Absent or empty | authd | Defaults to `LocalService`. |

peinit reads its own token with `kacs_open_self_token` requesting the
real token rather than any impersonation, and opens it query-only: it is
a template to copy from, never a thing to hand out.

## 4.1.1 Where a token is materialised

Materialisation happens at the point of use, per launched process, not
once per service. A service that runs a pre-exec hook, a main process,
and then a health check materialises three tokens.

| Context | Identity used |
|---|---|
| Main process | `Identity` |
| `ExecStartPre` / `ExecStartPost` | `HookIdentity` if set, otherwise `Identity` |
| Health checks | `Identity`, always |
| `ExecReload` external command | `Identity`, always |
| Ad-hoc jobs | The token JFS captured from the submitter |

Health checks and reload commands deliberately do not honour
`HookIdentity`. A health check reports on the service's own health and
should see what the service sees; a reload command acts on the running
service. `HookIdentity` exists for setup work — creating directories in
privileged locations, running a migration — which is a different job
from either.

> [!NOTE]
> `HookIdentity` typically grants *more* than the service itself.
> peinit does not validate filesystem permissions on hook binaries, so
> where it grants elevated privileges the administrator is responsible
> for the hook binary and its parent directories not being writable by
> anything lower-privileged.

If materialisation fails at any point — authd unreachable, an identity
that cannot be resolved, a KACS error — no child exists yet, and the
start fails with `ParentSetupFailure` for the main process or
`PreHookFailure` for a hook.

---

# 4.2 The SYSTEM Path

_Peios / Advanced Peios / peinit / Service Identity_

> peinit mints SYSTEM tokens itself, which is what breaks the bootstrap circle for registryd, lpsd, authd and eventd.

For `Identity=SYSTEM`, peinit mints a token itself. This is what breaks
the bootstrap circle: registryd, lpsd, authd and eventd all need tokens,
and authd — the thing that mints tokens — is one of them.

## 4.2.1 Minting

peinit reads its own token as a template and builds a new **primary**
token carrying the same identity: user SID `S-1-5-18`, the same group
list, the same privilege set, the same integrity level. The mint
requires `SeCreateTokenPrivilege`, which the boot SYSTEM token carries;
the kernel refuses the call with `EPERM` otherwise.

Two details of the copy matter.

**The logon session comes from the token's statistics.** peinit takes
the `auth_id` from the source token's `TokenStatistics` — not from the
independent `interactivity_scope` field, and not from a hard-coded
well-known SYSTEM LUID. The minted token therefore stays associated
with the real SYSTEM logon session peinit was given at boot, while
carrying its own interactivity scope, which is zero for a platform
service. Substituting either of the other two values would associate
platform services with a session that does not exist.

**The logon SID group is dropped from the copy.** The kernel re-appends
the session's logon SID when it creates the token, and rejects a create
whose group list already contains it. So peinit filters that group out
of the template before building.

peinit also asserts that its own token is a primary token and that its
user SID really is `S-1-5-18` before minting, and fails the start with a
message naming what it found otherwise. PID 1 minting from something
that is not the boot SYSTEM token is not a situation to proceed from.

The minted token is fully independent. The privilege restriction that
follows (§4.5) operates on it alone and cannot affect peinit's own.

> [!NOTE]
> Minting is an interim mechanism. The intended model is for peinit to
> *derive* the token from its own handle, through a KACS
> duplicate-with-additions operation — kernel-attested as a descendant
> of peinit's real token, and requiring no token-minting privilege at
> all. That operation does not exist in KACS yet, so peinit mints.

---

# 4.3 The authd Path

_Peios / Advanced Peios / peinit / Service Identity_

> Every non-SYSTEM token comes from authd — peinit resolves no identities itself, and the interface belongs to authd.

For any identity other than `SYSTEM`, the token comes from authd. peinit
does not resolve identities, does not know whether a principal is local
or from a domain, and does not want to: routing is authd's whole
purpose.

What peinit requires of authd is:

1. peinit sends the `Identity` value verbatim.
2. authd routes it to an identity source — a built-in for the
   well-known principals, lpsd for local accounts, a connector for
   domain accounts.
3. The source returns the principal's user SID and group SIDs.
4. authd mints a KACS token, creates a logon session, adds the
   per-service SID to the group list, and returns the token descriptor
   to peinit.

Every non-SYSTEM service start depends on this, and every one of them
fails if authd is unavailable when the token is needed. Platform
services are unaffected, because they never take this route.

## 4.3.1 The interface is authd's

The steps above describe what peinit needs, not how it asks. authd owns
the request and response schema, the socket path, and the descriptor
passing mechanism. No authd specification exists yet — authd's design
was deliberately deferred until KACS and the registry had settled — so
this path is not implementable from this manual alone.

## 4.3.2 The current implementation

peinit's authd client is a placeholder. It ignores the requested
identity and returns a freshly minted SYSTEM token, taking the §4.2 path
for every service.

The consequence is that every service currently runs with user SID
`S-1-5-18` and peinit's full privilege set, whatever its definition
says. `RequiredPrivileges` still applies, and is currently the only
thing that reduces what a service can do. A service that does not set it
runs fully privileged.

Two things follow that are worth stating plainly, because both are
easy to reason wrongly about:

- **Status output reports the declared identity, not the effective
  one.** The job's resolved identity string is what appears in a status
  query and in a `job.created` event, and it says `LocalService` for a
  service running on a SYSTEM token.
- **The per-service SID is still correct.** peinit computes it from the
  service name and adds it to the minted token (§4.4), so per-service
  ACLs behave as designed even while the user SID does not.

The placeholder also interacts with socket protection in a way worth
knowing about. peinit's sockets are reachable only by SYSTEM (§13.3),
so a service on a correctly resolved non-SYSTEM token could not reach
the notification socket to report readiness. Today every service holds
a SYSTEM token, so the question does not arise — which means the two
have to be resolved together rather than one at a time.

---

# 4.4 Per-Service SIDs

_Peios / Advanced Peios / peinit / Service Identity_

> The SID derived from a service's name and carried in its token's group list — why it exists, and the uppercasing rule.

Every service token carries a SID derived from the service's name, in
its group list, alongside whatever the principal's own identity brings.

The derivation is the one KACS defines (Peios Kernel TRM §3.2). The
authority is `S-1-5-80`. The service name is uppercased and encoded as
UTF-16LE, SHA-1 is taken over those bytes, and the 20-byte digest is
split into five little-endian 32-bit sub-authorities:

```
S-1-5-80-<sub1>-<sub2>-<sub3>-<sub4>-<sub5>
```

peinit computes this itself, from the name alone, with no involvement
from anything else. authd computes the same value independently when it
mints a token, and the two implementations are pinned against each other
by a shared test vector.

## 4.4.1 Why they exist

Per-service SIDs are what make access control useful when services share
a principal. Every platform daemon runs as SYSTEM, and every service
with no `Identity` runs as `LocalService`; without something to tell
them apart, an ACL could grant a right to "LocalService" and thereby
grant it to a dozen unrelated services.

With a per-service SID, an ACE can name one specific service. It costs
nothing — no account, no registry entry, no allocation — because it is a
hash of a name that already has to be unique.

They are load-bearing in more than access checks. peinit uses the
service SID directly when it provisions a service's runtime directories,
stamping `/run/<name>` with a descriptor that grants full access to
SYSTEM, Administrators, and that service's SID and nothing else (§3.3).

## 4.4.2 The uppercasing rule

The name is uppercased before encoding, using full Unicode case mapping
— the one that expands `ß` to `SS` and `ﬁ` to `FI` rather than mapping
each code unit in place. peinit uppercases in the code-point domain,
before the UTF-16 encoding, so any expansion happens first.

For an ASCII service name, which is every real service, the choice is
invisible. It becomes visible only for a name containing a character
whose uppercase form is longer than itself.

---

# 4.5 Privilege Restriction

_Peios / Advanced Peios / peinit / Service Identity_

> peinit only ever removes privileges from a token and never adds one, on either identity path.

`RequiredPrivileges` is a list of the privileges a service needs.
Everything else is removed from its token before exec.

## 4.5.1 Subtractive only

peinit removes privileges. It never adds one, on either path — there is
no code that constructs anything but a removal. A service cannot acquire
a privilege by naming it in `RequiredPrivileges`; if the token it was
given does not have it, listing it changes nothing.

The removal targets the token's **present** bitmask, through
`KACS_IOC_ADJUST_PRIVS` on the token descriptor — one `kacs_priv_entry`
per removed privilege, carrying the privilege-removed attribute. The
right required on the descriptor is `KACS_TOKEN_ADJUST_PRIVS`, which a
freshly minted token always has.

peinit does not use `KACS_IOC_RESTRICT`. That builds a restricted-SID
token — a different mechanism with different semantics — and does not
touch the privilege bitmask at all. It is available in the bindings and
would be a plausible-looking mistake.

Removing a privilege clears its present, enabled and enabled-by-default
bits together, and irreversibly. The privileges that *survive* keep the
enable state their source gave them: peinit does not enable, disable or
re-order anything. Enable policy belongs to whoever minted the token.

peinit iterates all sixty-four privilege bits rather than only the ones
it has names for, so a privilege this build does not know about is
stripped along with the rest. The safe direction is to remove what was
not asked for, including what cannot be named.

If `RequiredPrivileges` is absent, peinit does not query or adjust the
token at all, and the source's default privilege set stands unchanged.

## 4.5.2 Names

Privilege names are matched case-sensitively against the published
privilege table. A name that does not match exactly is not silently
ignored: it fails token materialisation, and therefore fails the service
start.

Two privileges KACS enforces are absent from the published table —
`SeTakeOwnershipPrivilege` and `SeRelabelPrivilege` — and so cannot be
named in `RequiredPrivileges` at all. A service that needs either
declares nothing and takes the source token's defaults, or fails to
start if it tries to name one.

---

# 4.6 Service Security Descriptors

_Peios / Advanced Peios / peinit / Service Identity_

> A service carries two independent descriptors answering different questions — the rights, the default, the check, and hot reload.

A service carries two independent descriptors, and they answer different
questions.

**The registry key descriptor** on `Machine\System\Services\<name>`
controls who may read and write the service's *definition*. It is
enforced by LCS at key-open time and is not peinit's concern — peinit
reads definitions as SYSTEM.

**The ServiceSecurity descriptor** controls who may perform *runtime
operations* on the service through the control interface. It is stored
as a binary `ServiceSecurity` value on the same registry key, but
enforced by peinit rather than by LCS.

The two are genuinely independent. An administrator might be able to
query a service's status without being able to read its configuration,
or the reverse. Runtime control and configuration access are separate
concerns and there is no reason for one to imply the other.

## 4.6.1 Access rights

| Right | Bit | Grants |
|---|---|---|
| `SERVICE_QUERY_STATUS` | 0x0001 | Query state, PID, cause, health, warnings. |
| `SERVICE_START` | 0x0002 | Start the service. |
| `SERVICE_STOP` | 0x0004 | Stop the service. |
| `SERVICE_INTERROGATE` | 0x0008 | Reload the service. |
| `SERVICE_ALL_ACCESS` | 0x000F | The union of the four. |

Restart requires `SERVICE_START` and `SERVICE_STOP` together. Reset
requires `SERVICE_STOP`, because clearing a Failed or Abandoned state is
the tail of stopping something rather than the head of starting it.

The generic mapping peinit passes to AccessCheck:

| Generic right | Maps to |
|---|---|
| `GENERIC_READ` | `SERVICE_QUERY_STATUS` |
| `GENERIC_WRITE` | `SERVICE_START` \| `SERVICE_STOP` \| `SERVICE_INTERROGATE` |
| `GENERIC_EXECUTE` | `SERVICE_START` \| `SERVICE_STOP` \| `SERVICE_INTERROGATE` |
| `GENERIC_ALL` | `SERVICE_ALL_ACCESS` |

## 4.6.2 Inheritance and the default

A service whose definition carries no `ServiceSecurity` value takes the
one on `Machine\System\Services` itself. The lookup is a single step to
that key, not a walk up the hierarchy, which is exact for the flat
layout definitions actually use.

If that key has no `ServiceSecurity` either, peinit applies a built-in
default:

```
O:SY G:SY D:(A;;GA;;;SY)(A;;0x0005;;;BA)
```

SYSTEM gets full access. Administrators get `SERVICE_QUERY_STATUS` and
`SERVICE_STOP` — query and stop, but not start and not reload. The
asymmetry is deliberate: stopping something that is misbehaving is a
containment action, and starting something is a change.

## 4.6.3 The check

When a control command arrives, peinit:

1. Takes the caller's token, captured when the connection was accepted.
2. Resolves the target service and its ServiceSecurity descriptor. A
   command naming no definition and no addressable definition-removed
   entry returns `UNKNOWN_SERVICE`; peinit does not invent a descriptor
   to check against.
3. Calls AccessCheck with the caller's token, that descriptor, the
   generic mapping above, and the right the command needs.
4. On denial, returns `ACCESS_DENIED` and records the attempt as an
   `access.denied` event carrying the caller's SID, the target, the
   requested right by name, the requested access bits and the granted
   bits.
5. On grant, proceeds.

## 4.6.4 Hot reload

`ServiceSecurity` changes take effect on the next control request, with
no restart. A registry change notification triggers a configuration
reload, and the reload re-reads every descriptor. There is no cached
decision to invalidate — the check runs against the current descriptor
every time.

## 4.6.5 Filtering, not denying

`list` returns only the services the caller has `SERVICE_QUERY_STATUS`
on. Services the caller cannot query are **omitted**, not denied: a
caller with no query rights anywhere receives an empty list and a
successful response. The denials are recorded as audit events rather
than surfaced to the caller, because reporting them would answer the
question the filtering exists to avoid answering.

---

# 4.7 The Control Descriptor

_Peios / Advanced Peios / peinit / Service Identity_

> Shutdown and configuration reload are checked against peinit's own descriptor rather than any service's.

Two operations are not about any one service: shutting the system down,
and re-reading the configuration. They are checked against peinit's own
descriptor, stored at `Machine\System\Init\ControlSecurity` as a binary
value.

## 4.7.1 Access rights

| Right | Bit | Grants |
|---|---|---|
| `SYSTEM_SHUTDOWN` | 0x0001 | Initiate poweroff, reboot or halt. |
| `SYSTEM_RELOAD_CONFIG` | 0x0002 | Re-read all definitions from the registry. |

The generic mapping:

| Generic right | Maps to |
|---|---|
| `GENERIC_READ` | nothing |
| `GENERIC_WRITE` | `SYSTEM_RELOAD_CONFIG` |
| `GENERIC_EXECUTE` | `SYSTEM_SHUTDOWN` |
| `GENERIC_ALL` | both |

`GENERIC_READ` maps to nothing because there is nothing to read: the
control descriptor governs two actions and no queries. A grant of
`GENERIC_READ` on it is not an error, it simply conveys no access.

## 4.7.2 The default

Absent a value in the registry, peinit applies:

```
O:SY G:BA D:(A;;0x0003;;;SY)(A;;0x0003;;;BA)
```

SYSTEM and Administrators both get shutdown and reload-config. Unlike
the ServiceSecurity default, this one is symmetric — an administrator
who can stop services one at a time can already stop the system, so
withholding shutdown would be theatre.

## 4.7.3 Loading

peinit loads the descriptor during Phase 2 boot and hot-reloads it on
registry change notification, on the same path as the service
descriptors. Until it is loaded — during Phase 1 and the early part of
Phase 2 — the built-in default applies, which matters because the
control socket exists from Phase 1 infrastructure setup onwards.

---

# 5.1 The Cgroup Tree

_Peios / Advanced Peios / peinit / Starting a Service_

> peinit uses cgroups v2 for exactly two things — knowing which processes belong to a service, and killing them all at once.

peinit uses cgroups v2 for exactly two things: knowing which processes
belong to a service, and killing all of them at once. It does no
resource accounting and sets no limits.

Every service gets a tree:

```
/sys/fs/cgroup/peinit/<cgroup-id>/          service root
/sys/fs/cgroup/peinit/<cgroup-id>/main/     the main process
/sys/fs/cgroup/peinit/<cgroup-id>/hooks/    hooks and reload commands
/sys/fs/cgroup/peinit/<cgroup-id>/health/   health check invocations
/sys/fs/cgroup/peinit/<cgroup-id>/checks/   pre-start filesystem check helpers
```

The sub-cgroups satisfy cgroups v2's "no internal processes" rule, which
applies whenever controllers are enabled, and give hooks and probes
containment of their own so that killing one does not touch the service.

They are not all created at once. The root and `hooks/` are created when
the first thing needs them — the first pre-exec hook, if there is one —
and `main/` and `health/` when the main process launches.

## 5.1.1 The cgroup id

`<cgroup-id>` is the service name with every byte outside
`[A-Za-z0-9._-]` percent-encoded as `%` plus two uppercase hex digits.
Service names are already restricted to that set (§3.1), so in practice
the id equals the name.

The encoding is a defensive guarantee that distinct names always map to
distinct, cgroup-safe ids. `%` is not itself in the safe set, so it
escapes to `%25` and the encoding is prefix-free. That is what makes it
injective, unlike a plain substitution of `/` for `-`, under which `a/b`
and `a-b` would collide.

The id is internal. The name a user sees is unchanged.

## 5.1.2 Generations

A cgroup whose processes survived SIGKILL cannot be removed — `rmdir` on
it fails with `EBUSY`. When peinit detects that a service's tree still
has live processes after the post-kill deadline, it records the leak and
increments the service's cgroup generation. The next start uses a fresh tree:

```
/sys/fs/cgroup/peinit/<cgroup-id>.gen<N>/
```

Old leaked trees persist until the next reboot.

The generation counter advances once per recorded leak rather than once
per restart, and leaks are deduplicated by path and kind. A single
failed start can record two — one for `hooks/` and one for the service
tree — so the number can advance by more than one at a time.

Because `.` is a legal character in a service name and the generational
suffix uses one, the tree *path* is not injective the way the id is: a
service literally named `app.gen1` and generation 1 of a service named
`app` resolve to the same directory.

---

# 5.2 Pre-Start Evaluation

_Peios / Advanced Peios / peinit / Starting a Service_

> Evaluating conditions and asserts before anything is forked — the two check mechanisms, and how results are cached.

Before anything is forked, peinit evaluates the service's conditions and
asserts (§3.5). The definition comes from the in-memory cache, so this
never touches the registry.

1. Conditions are evaluated. Any failure transitions the service to
   Skipped and abandons the start. Skipped satisfies dependents.
2. If every condition passed and the service has asserts, they are
   evaluated. Any failure transitions the service to Failed with cause
   `AssertionError` and abandons the start.

Only when both pass does the pre-exec sequence continue.

## 5.2.1 Where the transition happens

For a fresh start from Inactive, the evaluation gates the transition:
the service becomes Starting only after the checks pass, so a skipped
service goes straight Inactive → Skipped.

For an activation that is already Starting — the start leg of a restart,
for instance — the transition has already happened, and the evaluation
gates further progress instead. A restart whose conditions no longer
hold therefore passes through Starting on its way to Skipped, where a
fresh start would not.

## 5.2.2 Two check kinds, two mechanisms

`registry:` checks resolve against the in-memory model. Since a
non-cacheable key is rejected when the definition is read, this
evaluation never needs a live read.

Filesystem checks — `path:`, `file:`, `directory:` — run in a forked
helper. peinit clones it with `CLONE_PIDFD | CLONE_INTO_CGROUP` into the
service's `checks/` sub-cgroup, exactly as it launches anything else.
The helper stats the paths and writes the results to a non-blocking
pipe; peinit watches the pipe and the helper's pidfd through epoll.

`PreStartCheckTimeout` bounds the helper, at 5 seconds by default. On
expiry every check still outstanding is marked **not satisfied** — the
fail-safe direction — and re-evaluated on that basis, so a condition
skips the service and an assert fails it. peinit then kills the helper's
cgroup and unregisters the result descriptor.

A helper that survives the kill, stuck in uninterruptible sleep, has its
cgroup abandoned and recorded exactly as a leaked hook or health cgroup
is (§5.7).

## 5.2.3 Caching

The results are computed once, before the service's dependencies are
started, and reused for the rest of that activation. A service that
waits a long time on a dependency starts on the answer that was true
when the wait began.

Filesystem checks are gathered in one helper run, from the conditions
and the asserts together. There is only ever one run — the completion
path evaluates both lists against the results it gets back, and there is
no mechanism to ask for a second — so both lists' paths have to be
stat'd before it. A path named in both is stat'd once.

A check with no result counts as not satisfied, which is the fail-safe
direction for a timeout and is why the gather has to be complete: an
omitted path is indistinguishable from a path that is not there.

---

# 5.3 The Pre-Exec Sequence

_Peios / Advanced Peios / peinit / Starting a Service_

> Everything between deciding to start a service and its binary running — timeouts, runtime directories, hooks and the fork.

Everything between "peinit decides to start service X" and "X's binary
is running". The service is in Starting throughout.

## 5.3.1 Step 1: Arm the start timeout

`StartTimeout` covers the entire remaining sequence — pre-hooks, fork,
exec, and the readiness wait. A single deadline, measured from the
operation's creation rather than from this point, bounds all of it.

Expiry aborts the start and kills the service's cgroup tree. The cause
recorded depends on where the deadline landed: a timeout during
pre-hooks is `PreHookFailure`; one during the readiness wait or the main
process's execution is `ReadinessTimeout`.

## 5.3.2 Step 2: Provision runtime directories

If the definition lists `RuntimeDirectories`, peinit creates each one
under `/run` and applies its descriptor (§3.3) before anything else in
the launch. Failure classifies as `ParentSetupFailure`.

## 5.3.3 Step 3: Run pre-exec hooks

Each `ExecStartPre` command runs in sequence, forked into `hooks/`. A
token is materialised for each at the point of use, from `HookIdentity`
if set and `Identity` otherwise (§4.1); a failure there fails the hook
and the service with `PreHookFailure`.

Any hook exiting non-zero kills the entire service cgroup tree —
cleaning up whatever grandchildren the hook left — and fails the service
with `PreHookFailure`.

When every hook has succeeded, peinit kills the `hooks/` sub-cgroup
before the main process starts, so a hook that forked something into the
background does not become part of the service.

## 5.3.4 Step 4: Materialise the service token

The main process's token, per §4.1. A failure means no child exists and
the service fails with `ParentSetupFailure`.

If a later parent-side step fails before the fork, peinit closes the
token descriptor exactly once. A failure to close is retained as cleanup
evidence and does not change how the start is classified — the
classification describes what went wrong with the start, and a failed
close is a separate fact about the same failure.

## 5.3.5 Step 5: Create the cgroups and the error pipe

The `main/` and `health/` sub-cgroups are created, and a
`pipe2(O_CLOEXEC)` error pipe. The parent keeps the read end,
non-blocking; the child will hold the write end.

The pipe is how the child reports a failure it hits after fork but
before exec. If exec succeeds, the write end closes automatically —
that is what `O_CLOEXEC` is for — and the parent reads EOF, which means
setup succeeded. If setup fails, the child writes a structured error
first.

**The payload is exactly eight bytes, written with one `write(2)`:**

| Bytes | Content |
|---|---|
| 0–3 | little-endian `u32`, the child setup step identifier |
| 4–7 | little-endian `i32`, a positive Linux errno |

The child writes at most one payload — the first reportable failure —
and then exits. The parent treats a non-EOF payload that is not exactly
eight bytes, or carries an unknown step identifier, or an errno of zero
or less, as malformed evidence and fails closed with `PreExecFailure`.

The step identifiers:

| Id | Step |
|---|---|
| 1 | Close the read end of the error pipe |
| 2 | Set the standard streams |
| 3 | Reset the signal environment |
| 4 | Install the service token |
| 5 | Set the resource limits |
| 6 | Set `oom_score_adj` |
| 7 | Set the working directory |
| 8 | *reserved* |
| 9 | Confirm `NOTIFY_SOCKET` |
| 10 | Inject stored descriptors |
| 11 | Exec the binary |
| 12 | Create a session |
| 13 | Acquire the controlling terminal |

Identifier 8 is reserved and never emitted: the environment is built in
the parent and applied by `execve`, so there is no step in the child
that could fail. Identifiers 12 and 13 are the two terminal steps, which
occur only for a service with a `TTYPath`.

If `pipe2` fails, no child exists and the service fails with
`ParentSetupFailure`.

## 5.3.6 Step 6: Fork

`clone3(CLONE_PIDFD | CLONE_INTO_CGROUP)`, targeting `main/`. This does
two things atomically: it returns a pidfd for the child, and it places
the child directly into `main/` at creation. There is no window in which
the child exists without a pidfd, and none in which it runs or execs in
peinit's own cgroup.

Placing the child at creation also avoids the alternative, which would
be having the child write its own PID into `cgroup.procs` — something
its post-installation token could not do.

A `clone3` failure means no child exists: `ParentSetupFailure`, with
`EMFILE`/`ENFILE`, `EAGAIN` and `ENOMEM` the usual causes.

Immediately after `clone3` returns, in the parent and on either outcome,
peinit closes the token descriptor and the `main/` cgroup descriptor,
exactly once each. The child relies on close-on-exec and its own exit
instead. A failure to close is cleanup evidence and does not change how
the start is classified.

## 5.3.7 Step 7: The parent after the fork

1. Close the write end of the error pipe.
2. Register the read end with the event loop. peinit does not block on
   it — signals, control traffic, timers and log pipes stay serviceable
   while the child's setup is pending.
3. When the read end becomes readable or hangs up, read it
   non-blocking:
   - **Would block:** leave the source registered, the job stays in
     pending setup.
   - **EOF:** exec succeeded. Record the pidfd on the job, emit
     `job.started`, and only then apply readiness side effects such as
     Simple/Alive activation.
   - **Data:** setup failed. Parse the step and errno, log the specific
     failure, fail the service with `PreExecFailure`.

While setup is pending the job is not Running, and no dependent that
waits on this service's readiness is released.

## 5.3.8 Steps 8 and 9

The child's own path is §5.4. What follows exec is the readiness wait
and the post-readiness work:

- **Simple with `Readiness=Notify`:** wait for `READY=1`, then Active.
- **Simple with `Readiness=Alive`:** Active as soon as exec succeeds.
- **Oneshot:** wait for the exit. Success is Completed; without
  `RemainAfterExit` it then goes Inactive once dependents are released.
  A non-zero exit is Failed.

On readiness or successful exit, peinit runs `ExecStartPost` in sequence
into `hooks/`, releases the service's dependents, kills `hooks/`, and —
for a Simple service only — arms the watchdog if `WatchdogTimeout` is
non-zero and the health check timer if `HealthCheck` is set.

A post-hook that fails is logged and does not fail the service. The
state transition to Active or Completed happens before the post-hooks
run, so a service is already Active while its post-hooks are executing.

## 5.3.9 Failures before the fork

Steps 2, 4, 5 and 6 can all fail with no child in existence. peinit
handles all four entirely in the parent: clean up whatever cgroups were
created, fail the service with `ParentSetupFailure`, and return the
error to the caller. These are system-level resource problems — file
descriptor limits, PID limits, memory, cgroup filesystem errors — rather
than anything about the service.

---

# 5.4 The Child Path

_Peios / Advanced Peios / peinit / Starting a Service_

> The deliberately minimal straight line between clone3 returning and execve — streams, signals, oom_score_adj and the environment.

Between `clone3` returning in the child and `execve`, peinit runs a
straight line of setup. The path is kept minimal — no logging, no
complex library calls — because it runs after a fork, where very little
is safe to do.

Each step reports through the error pipe with the identifier from §5.3
if it fails, then exits: `_exit(126)` for a setup failure, `_exit(127)`
for a failed exec.

| # | Step | Id |
|---|---|---|
| 1 | Close the read end of the error pipe | 1 |
| 2 | `setsid()` — only with a `TTYPath` | 12 |
| 3 | Set the standard streams | 2 |
| 4 | `ioctl(TIOCSCTTY)` — only with a `TTYPath` | 13 |
| 5 | Reset the signal environment | 3 |
| 6 | Install the KACS token, then close its descriptor | 4 |
| 7 | Set `RLIMIT_NOFILE` and `RLIMIT_CORE` | 5 |
| 8 | Set `oom_score_adj` | 6 |
| 9 | Change the working directory | 7 |
| 10 | Confirm `NOTIFY_SOCKET` is present in the environment | 9 |
| 11 | Inject stored descriptors from fd 3 upward | 10 |
| 12 | `execve` | 11 |

## 5.4.1 The terminal steps

`setsid()` comes first, and its position is load-bearing in two
directions. It has to precede the stream setup, because `setsid()` drops
any controlling terminal the child inherited — doing it afterwards would
throw away the terminal just attached. And it has to precede
`TIOCSCTTY`, which requires a session leader that does not already own a
controlling terminal.

`TIOCSCTTY` is passed a literal zero argument, which is what makes it
unable to steal a terminal already owned by another session.

Without a `TTYPath` neither step runs and the service stays in peinit's
session.

## 5.4.2 The standard streams

Without a `TTYPath`: stdin from `/dev/null`, stdout and stderr onto the
write ends of the service's output pipes, and every inherited pipe end
that is no longer needed closed.

With a `TTYPath`: all three streams onto the opened terminal, and the
`/dev/null` descriptor and both pipe pairs closed. A terminal-attached
service's output is therefore **not** captured for logging — it goes to
the terminal, which is the point of asking for one.

## 5.4.3 The signal environment

peinit blocks every signal for its signalfd (§12.3) and the child
inherits that mask across the fork. Step 5 empties the mask and resets
every resettable disposition to `SIG_DFL`. A service starting with
signals blocked, or with PID 1's handling in place, is one of the
classic ways for a daemon to behave inexplicably.

## 5.4.4 oom_score_adj

`-1000` — OOM-immune — for an `ErrorControl=Critical` service, and `0`
for everything else. A Critical service is one whose loss reboots the
machine, so letting the OOM killer choose it would convert memory
pressure into a reboot.

## 5.4.5 The environment

There is no step that sets the environment, which is why identifier 8 is
reserved and never emitted. peinit builds the environment in the parent
(§5.5) and hands it to `execve` as `envp`, so it arrives with the exec
rather than being installed beforehand. Step 10 is a check rather than a
set: it confirms `NOTIFY_SOCKET` is present in the prebuilt environment
and fails with a synthetic `EINVAL` if it is not.

## 5.4.6 What the child does not inherit

A service inherits only what peinit hands it: its standard streams and
any descriptors injected from the fd store. Everything else peinit holds
is created close-on-exec — the control socket and every accepted
connection, the notification socket, the epoll instance, the signalfd,
every timerfd, both pidfds, every pipe, and every stored descriptor
until it is deliberately un-marked at injection.

The signal reset and the close-on-exec discipline together are what make
a service start from a clean context rather than from PID 1's
privileged one.

The exception is a descriptor opened through the Peios native file
interface, which returns without close-on-exec set. peinit repairs that
where it opens input devices for the power button; the JFS device
descriptor and each service's own cgroup directory descriptor are not
repaired, and are inherited across exec.

---

# 5.5 The Base Environment

_Peios / Advanced Peios / peinit / Starting a Service_

> Every service environment is built from scratch in four layers, lowest precedence first, with nothing inherited.

peinit constructs every service and hook process's environment from
scratch, in four layers, lowest precedence first. Nothing is inherited:
peinit's own startup environment holds `TERM` and nothing else (§2.1),
and none of it is passed through.

## 5.5.1 Layer 1: the compiled-in base

One variable:

| Variable | Value |
|---|---|
| `PATH` | `/sbin:/bin` |

Executables are addressed through the root-level StrataFS runtime views.
Package storage paths under `/usr` are deliberately not on the default
search path.

## 5.5.2 Layer 2: global environment variables

Each value under `Machine\System\Init\EnvVars\` becomes a variable: the
value name is the variable name, the `REG_SZ` data is the value. An
`EnvVars\PATH` overrides the compiled-in `PATH`; every other name adds.

A malformed entry — an empty name, or a name containing `=` — fails the
whole layer, which at boot means recovery mode.

**registryd does not receive this layer.** The exemption is a trust
rule, not an availability one. Write access to `EnvVars\` is equivalent
to compromising every service peinit starts: `LD_PRELOAD`,
`LD_LIBRARY_PATH` and their relatives are not filtered, because the
key's Security Descriptor is meant to be the control boundary. A key
that could inject into the daemon that enforces who may write it would
make that boundary self-referential.

The exemption is narrow, and matches on two things at once: the job's
resolved identity is `SYSTEM` and its service name is `registryd`. A
non-platform service that happens to be called `registryd` receives the
ordinary layering. It matches on the *job*, so a hook of registryd's
running under a non-SYSTEM `HookIdentity` would receive the layer; and
it matches the resolved identity string, so a definition naming
`S-1-5-18` literally rather than `SYSTEM` would not be exempt.

registryd is launched in Phase 1, before `EnvVars` has been read at all,
so the exemption is only observable on a restart.

## 5.5.3 Layer 3: the service's own Environment

The definition's `Environment` entries, overriding both layers below.

## 5.5.4 Layer 4: protocol variables

`NOTIFY_SOCKET`, always. `LISTEN_FDS` and `LISTEN_FDNAMES`, only when
descriptors are being injected from the fd store.

These have the highest precedence and are inserted after both
configurable layers, so a service cannot override `NOTIFY_SOCKET` and
break its own notification protocol. The guard is insertion order rather
than a reserved-name check, which means the two fd-store variables are
protected only when they are actually being set — with no descriptors to
inject they are not inserted, and a value from either configurable layer
reaches the child unchanged.

`LISTEN_PID`, which a conforming `sd_listen_fds` implementation checks
against its own PID before trusting `LISTEN_FDS`, is not set.

## 5.5.5 What peinit does not set

Not `HOME`, `USER`, `LOGNAME`, `SHELL` or `TERM`. Peios identity is a
KACS token — a SID — rather than a passwd entry, so there is no
canonical home directory or login shell to populate. A service that
needs one supplies it through `EnvVars\` or its own `Environment`.

## 5.5.6 Hooks and probes

Hooks, health checks and reload commands are built through the same
path, so they receive the identical environment: the same layers, the
same `NOTIFY_SOCKET`, and the service's `WorkingDirectory`,
`LimitNOFILE`, `LimitCORE` and `RequiredPrivileges`. They never receive
`LISTEN_FDS` — stored descriptors go to the main process only.

## 5.5.7 When changes apply

The global layer is a snapshot refreshed at boot and on reload-config,
and both it and the per-service `Environment` take effect at a service's
next start. Neither is applied to a running process.

> [!NOTE]
> Write access to `Machine\System\Init\EnvVars\` is equivalent to
> compromising every service on the system. peinit does not filter
> variable names, consistent with the registry's write-authority threat
> model — the key's descriptor is the boundary. The recommended default
> is SYSTEM full control and Administrators read-only.

---

# 5.6 Health Checks

_Peios / Advanced Peios / peinit / Starting a Service_

> A watchdog says a service is still ticking; a health check says it still works — execution, overlap, failure and the flap constraint.

A watchdog tells peinit that a service is still ticking. A health check
tells it that the service still works. They address different failures:
a process can be alive and responsive to its own event loop while having
lost its database connection, wedged in a bad state, or started
returning errors to everyone.

## 5.6.1 Execution

The health check command runs with the **service's own token** — never
`HookIdentity` — so it checks the service's health from the service's
own vantage point.

Each invocation runs in an ephemeral `health/` sub-cgroup under the
service's tree, as a child of peinit rather than of the service. When
the check completes or times out, peinit kills the whole sub-cgroup,
which cleans up anything the check spawned.

## 5.6.2 Overlap

If the previous check is still running when the next interval fires, the
new one is skipped and nothing is counted. A check exceeding
`HealthCheckTimeout` has its sub-cgroup killed and **is** counted as a
failure.

A launch failure — a token that could not be materialised, a fork that
failed — is also counted as a failure and escalates immediately.

## 5.6.3 Failure

`HealthCheckRetries` consecutive failures mark the service unhealthy.
An unhealthy service is restarted through the ordinary restart policy:
`RestartPolicy`, exponential backoff and throttling all apply, exactly
as for a crash. The failure count resets the moment a check succeeds.

Escalation kills the service's **root** cgroup rather than just `main/`,
so it takes hooks and probes with it.

## 5.6.4 The flap constraint

Restart throttling is what stops a service flapping — failing checks,
restarting, passing initial checks, failing again. But it only works if
the failure cycle is shorter than `RestartWindow`, because otherwise the
service stays healthy long enough between failures to reset the restart
counter, `RestartMaxRetries` is never reached, and it restarts forever.

So this relationship has to hold:

```
HealthCheckRetries × HealthCheckInterval < RestartWindow
```

It is enforced as an error rather than a warning, in both places a
definition can arrive. At boot, a violating service is blocked with
cause `ValidationError` and never started. On reload-config, it is a
validation finding, and a finding rejects the entire reload.

The constraint is checked for any service that declares a `HealthCheck`,
including a Oneshot — even though health checks are scheduled only for
Simple services, so the check being constrained would never run.

> [!NOTE]
> Active health checks on `ErrorControl=Critical` services deserve
> caution. A false positive eventually exhausts the restart budget and
> reboots the machine. Critical services — registryd, authd, lpsd,
> eventd — are usually better served by a passive watchdog with a
> generous timeout: the service knows whether it is healthy and can stop
> sending keepalives when it decides it is not. peinit does not prevent
> health checks on Critical services and applies identical semantics to
> them, but the escalation path ends at a reboot.

---

# 5.7 Leaked Sub-Cgroups

_Peios / Advanced Peios / peinit / Starting a Service_

> A D-state process does not die when SIGKILLed — what happens to its cgroup, and how the leak becomes visible.

A process in uninterruptible kernel sleep — D-state, typically a hung
NFS mount or a failing disk controller — does not die when it is
SIGKILLed. Its cgroup cannot be removed while it is there.

peinit detects this through `cgroup.events`: after sending the kill it
arms a post-kill deadline, 5 seconds by default, and checks whether
`populated` is still 1 when the deadline fires.

## 5.7.1 What happens depends on which cgroup it is

For the **main process**, a survivor is fatal to supervision: the
service transitions to Abandoned with cause `ProcessUnkillable` and
peinit stops supervising it (§6.1).

For a **health check or a hook**, it is not. Those are diagnostic and
setup processes; they hold no service resources — no ports, no file
locks, no database connections — so a stuck one does not make the
service unmanageable. peinit orphans the sub-cgroup instead:

1. Marks it leaked, recording the path, the kind and the time.
2. Increments the service's cgroup generation, so the next start builds
   a fresh tree (§5.1).
3. Carries on supervising the service normally.

A leaked **pre-start check helper** is treated the same way: its
`checks/` sub-cgroup is recorded and the generation bumped, which matters
because otherwise the next start would build into a tree that still
contains the unkillable process.

The leaked cgroup stays in the hierarchy until the next reboot.

## 5.7.2 Visibility

Leaks are not silent. peinit both pushes one when it is detected and
keeps it queryable afterwards.

The push happens once, on first detection — recording is idempotent, so
a leak that is re-examined on a later cleanup pass is not announced
again:

- A **`cgroup.leaked` event** on the event stream, carrying the service,
  the sub-cgroup path, its kind, and the detection time in monotonic
  nanoseconds.

- A **console line** naming the same service, kind and path.

The pull side survives the moment of detection, for anyone who was not
watching:

- A **status query** includes a `warnings` array, one entry per leak,
  each an object with the sub-cgroup path, its kind, and the time of
  detection:

  ```json
  {"path": "/sys/fs/cgroup/peinit/jellyfin/health", "type": "health", "detected_at": "2026-06-01T12:34:56.123456789Z"}
  ```

- A **start command** on a service with leaks returns a warning in its
  acknowledgement, saying the service has leaked sub-cgroups from a
  previous generation and that this indicates an I/O problem needing
  investigation.

The `type` is the same vocabulary everywhere: `health` for a leaked
`health/` sub-cgroup, `hooks` for a leaked `hooks/` one, `helper` for a
pre-start check helper's `checks/`, and `service_tree` for a leaked
service root — the last being the most serious, since it means the whole
tree including `main/` could not be reclaimed.

Whichever way it reaches you, what the leak means is the same: something
underneath the service is not responding to the kernel, and no amount of
restarting the service will fix it.

---

# 6.1 States

_Peios / Advanced Peios / peinit / The State Machine_

> The ten states a service can be in, the three that satisfy dependents, and the invariants that hold across all of them.

Every service is in exactly one state. There are ten.

| State | Process? | Satisfies dependents? | Meaning |
|---|---|---|---|
| Inactive | No | No | Not started, or stopped cleanly and not set to restart. |
| Starting | Maybe | No | Activation in progress: checks, hooks, fork, or the readiness wait. The process may not exist yet. |
| Active | Yes | Yes | Running and ready. |
| Reloading | Yes | Yes | Re-reading configuration. Still satisfies dependents. |
| Stopping | Briefly | No | SIGTERM sent, awaiting exit or SIGKILL escalation. |
| Completed | No | Yes | Oneshot only. Exited successfully. |
| Backoff | No | No | A restart is pending; waiting out the backoff delay. |
| Failed | No | No | Exited abnormally with the restart policy exhausted or absent. |
| Abandoned | Yes, unkillably | No | SIGKILL sent and processes survived. Supervision has stopped, the cgroup is leaked. |
| Skipped | No | Yes | Conditions were not met. The service does not apply. |

## 6.1.1 Dependent satisfaction

Exactly three states satisfy dependents: **Active**, **Completed** and
**Skipped**. Nothing else does, and a dependent blocked on a `Requires`
target in any other state does not start.

Completed satisfies regardless of `RemainAfterExit` — a Oneshot without
it passes through Completed to release its dependents on the way to
Inactive, rather than skipping the state.

Skipped satisfies because a service whose conditions do not hold has
succeeded by not needing to run. Treating it as a failure would make
every conditional service a hazard to everything that depends on it.

Reloading satisfies because the process is still there and still
serving; a reload is a service telling itself to re-read a file, not an
outage.

Backoff does not, and the distinction from Failed matters: a service in
Backoff is *going* to start again, and its dependents wait rather than
failing. It is the state that makes a restart something other than a
transit through Failed.

## 6.1.2 Invariants

1. A service is in exactly one state at any moment. There is one state
   field and exactly one place in the implementation that assigns to it.
2. Only the transitions in §6.2 are performed. The assignment is gated
   by a central whitelist, so an unlisted transition is not merely
   avoided by convention — it cannot be written.
3. Only peinit transitions a service. The control socket produces
   operations; nothing outside peinit writes state.
4. A service object is securable independently of its process token. The
   ServiceSecurity descriptor governs who may manage the service; the
   process token governs what the service may reach. Neither implies
   anything about the other.
5. Readiness is per start generation. The generation increments on every
   transition into Starting, and a `READY=1` carrying a stale generation
   is rejected — so a notification from a previous incarnation can never
   be mistaken for this one's.

---

# 6.2 Transitions

_Peios / Advanced Peios / peinit / The State Machine_

> Every transition peinit performs — anything absent from the table is not performed — plus reset from Abandoned.

Every transition peinit performs. Anything not listed here is not
performed.

| From | To | Trigger |
|---|---|---|
| Inactive | Starting | Start command, dependency resolution, or a timer trigger. |
| Inactive | Skipped | A condition check failed. |
| Inactive | Failed | Validation, assertion, cycle or dependency failure — before any process existed. |
| Starting | Active | Readiness. Simple only: `READY=1` received, or the process exists under `Readiness=Alive`. |
| Starting | Completed | Oneshot exited successfully. Without `RemainAfterExit` it passes through, releasing dependents, then goes Inactive. |
| Starting | Skipped | A condition failed after the activation had already entered Starting. |
| Starting | Failed | An assert failed after entering Starting; or a timeout, hook failure, setup failure, or an exit before readiness. |
| Starting | Backoff | A startup failure with the restart policy allowing a retry and budget remaining. |
| Starting | Stopping | An explicit stop cancelling an in-progress start. A *restart* on a Starting service is queued rather than cancelling. |
| Starting | Failed | The shutdown wave SIGKILLed a starting service. |
| Active | Reloading | `ExecReload` issued, or SIGHUP delivered. |
| Active | Stopping | Explicit stop, conflict eviction, a bound dependency stopping, or shutdown. |
| Active | Backoff | A crash, a watchdog timeout, or health check failure, with a restart allowed and budget remaining. |
| Active | Failed | The same three, with `RestartPolicy=Never` or the budget exhausted. |
| Active | Inactive | A Simple service exited successfully and `RestartPolicy` is not Always. Cause `CleanExit`. |
| Active | Backoff | A Simple service exited successfully and `RestartPolicy=Always`. Cause `CleanExitRestart`. |
| Reloading | Active | Reload resolved: `READY=1`, the detection window expiring, the extended wait expiring, or the reload command exiting — success or failure. |
| Reloading | Stopping | The same triggers as Active to Stopping. The reload is cancelled. |
| Reloading | Backoff | The main process exited during the reload, with a restart allowed. |
| Reloading | Failed | The same, with no restart available. |
| Stopping | Inactive | The process exited after an explicit stop or a shutdown. |
| Stopping | Failed | The process exited after a conflict eviction or a bound dependency stopping. |
| Stopping | Abandoned | SIGKILL sent and `main/` still populated after the post-kill deadline. |
| Backoff | Starting | The backoff delay elapsed. Cause `RestartPolicy`. |
| Backoff | Inactive | An explicit stop cancelled the pending restart. |
| Completed | Inactive | `RemainAfterExit=0` and dependents released; or an explicit stop; or shutdown clearing it. |
| Completed | Starting | A start command or timer trigger re-running the Oneshot. |
| Failed | Starting | An explicit start, a bound dependency recovering, or a timer trigger. |
| Failed | Inactive | A reset command clearing the state. |
| Failed | Abandoned | A service SIGKILLed by the shutdown wave whose cgroup stayed populated past the post-kill deadline. |
| Abandoned | Inactive | A reset command. |
| Skipped | Inactive | A reset command, or an explicit start clearing Skipped before it re-evaluates the conditions. |

## 6.2.1 Things the table settles

**A restart never passes through Failed.** A restart-eligible failure
goes to Backoff, waits, and goes to Starting. Failed is reached only
when there will be no retry: `RestartPolicy=Never`, an invalid policy
for the cause, or an exhausted budget. This is why `OnFailure` (§6.3),
which fires on entry to Failed, does not fire on each retry — only when
the service finally fails out.

**A clean exit is not a crash.** A Simple service exiting zero goes to
Inactive under cause `CleanExit`, consulting neither the restart policy
nor the budget. It goes to Backoff only under `RestartPolicy=Always`,
and then with the distinct cause `CleanExitRestart`, so status and
events say plainly that the process succeeded and was restarted by
policy rather than that anything went wrong.

**A forced stop remembers why.** Stopping to Failed carries the cause
from the transition that started the stop — `ConflictEviction` or
`BindsToPropagation` — rather than a generic failure. A service that
lost a conflict and a service whose binding target went away are
distinguishable afterwards, which is what makes bound-dependency
recovery (§7.1) possible at all.

**A restart detours through Inactive.** The stop leg of an
administrator's restart ends in Inactive, and the start leg begins from
there, so a restarting service is briefly observable as Inactive.

**A crash before readiness may be retried.** A Simple process that exits
before signalling readiness is a `ProcessCrash` from Starting, and is
restart-eligible like any other, rather than a terminal startup failure.

## 6.2.2 Reset from Abandoned

Resetting an Abandoned service re-checks its `main/` sub-cgroup. If it
has finally emptied, peinit cleans up the whole service tree — `main/`,
`hooks/`, `health/`, then the root — and transitions to Inactive.

If it is still populated, peinit leaves the cgroup leaked, transitions
to Inactive anyway, and returns this warning in the operation
acknowledgement:

```
abandoned main cgroup for service <service> is still populated after
reset -- cgroup remains leaked; underlying D-state process requires
investigation
```

The warning is also written to the console, so a reset issued without
reading the response still leaves a trace of the still-leaked cgroup.

The re-check targets `main/`. Note that the two paths into Abandoned
probe different cgroups: an explicit stop checks `main/`, while the
shutdown wave checks the service root.

---

# 6.3 Transition Causes

_Peios / Advanced Peios / peinit / The State Machine_

> Every transition carries a cause — the taxonomy, which causes are restart-eligible, OnFailure, the loop guard and the logging contract.

Every transition carries a cause recording why it happened. peinit
tracks both the current state and the cause of the most recent
transition, and the cause determines restart eligibility, `OnFailure`
behaviour, and what an administrator is told.

## 6.3.1 The taxonomy

| Cause | Leads to | Meaning |
|---|---|---|
| `ExplicitStart` | Starting | An administrator, an `OnFailure` handler, or a boot plan started it. |
| `ExplicitStart` | Inactive | An explicit start on a Skipped service, clearing it so the conditions are re-evaluated. |
| `DependencyStart` | Starting | Started to satisfy another service's dependency. |
| `RestartPolicy` | Starting | An automatic restart after a backoff delay. |
| `BindsToRecovery` | Starting | A bound dependency returned to Active. |
| `Timer` | Starting | A timer trigger fired. |
| `ExplicitStop` | Stopping | An administrator requested a stop. |
| `ExplicitReload` | Reloading, Active | A reload was issued and resolved. |
| `ExplicitReset` | Inactive | An administrator cleared Failed, Abandoned or Skipped. |
| `ConflictEviction` | Stopping | A conflicting service started; this one lost. |
| `BindsToPropagation` | Stopping | A bound dependency stopped. |
| `ShutdownWave` | Stopping, Failed | The system is shutting down. Active and Reloading services go to Stopping; Starting services go straight to Failed. |
| `ProcessCrash` | Failed, Backoff | The main process exited unexpectedly. |
| `CleanExit` | Inactive | A Simple process exited successfully with a policy other than Always. |
| `CleanExitRestart` | Backoff | A Simple process exited successfully under `RestartPolicy=Always`. |
| `ReadinessTimeout` | Failed, Backoff | `StartTimeout` expired before readiness. |
| `WatchdogTimeout` | Failed, Backoff | A keepalive did not arrive in time. |
| `HealthCheckFailure` | Failed, Backoff | `HealthCheckRetries` consecutive failures. |
| `PreHookFailure` | Failed, Backoff | An `ExecStartPre` hook exited non-zero, or its token failed, or the start timed out during hooks. |
| `ParentSetupFailure` | Failed, Backoff | A parent-side failure before the fork. No child was created. |
| `PreExecFailure` | Failed, Backoff | Post-fork setup failed before exec, reported through the error pipe. |
| `DependencyFailure` | Failed | A `Requires` dependency entered Failed. |
| `RestartBudgetExhausted` | Failed | `RestartMaxRetries` reached. |
| `CycleDetected` | Failed | The service is part of a dependency cycle. |
| `ValidationError` | Failed | The definition failed graph validation. |
| `AssertionError` | Failed | A start-time assert failed. |
| `ConditionSkipped` | Skipped | A start-time condition failed. |
| `ProcessUnkillable` | Abandoned | Processes survived SIGKILL. |

## 6.3.2 Restart eligibility

Causes fall into four classes, and the class decides whether the restart
policy is consulted at all.

**Restart-eligible.** peinit consults `RestartPolicy` and the budget. If
a restart is allowed and the budget holds, the service goes to Backoff
and then to Starting; otherwise Failed.

`ProcessCrash`, `WatchdogTimeout`, `HealthCheckFailure`,
`ReadinessTimeout`, `PreHookFailure`, `PreExecFailure`,
`ParentSetupFailure`.

> [!NOTE]
> The startup failures are restart-eligible because a transient problem
> during startup should get another go. A pre-hook that failed because a
> network mount was briefly unavailable deserves a retry rather than an
> immediate give-up.

**Always-only.** `CleanExitRestart` applies when a Simple service exits
successfully and the policy is Always. It uses the same backoff and the
same budget as a failure, which is what stops a daemon that exits
cleanly in a tight loop from bypassing throttling entirely. It is never
treated as a `ProcessCrash`, and both status and events make clear the
process succeeded and was restarted only because the policy says so.

`CleanExit` is its non-restarting counterpart: straight to Inactive,
consulting neither policy nor budget.

**Budget-exempt.** `BindsToRecovery` takes a service from Failed to
Starting when its binding target returns. It is not subject to the
policy or the budget, because the service did not fail on its own — it
was stopped because its dependency went away.

**Never-restart.** peinit does not consult the policy at all:
`ExplicitStop`, `ExplicitReset`, `ShutdownWave`, `ConflictEviction`,
`BindsToPropagation`, `ProcessUnkillable`, `RestartBudgetExhausted`,
`ValidationError`, `CycleDetected`, `DependencyFailure`,
`AssertionError`, `ConditionSkipped`. Retrying cannot help with any of
them.

## 6.3.3 OnFailure

When a service enters Failed and its definition names an `OnFailure`
service, peinit starts that service — with the exceptions below.
`OnFailure` fires on *entry* to Failed, and Failed to Failed is not a
transition, so it fires at most once per failure.

It does not fire for:

- **`ShutdownWave`** — no new service starts during shutdown, so a
  fallback would be both impossible and pointless.
- **`ValidationError`, `CycleDetected`, `DependencyFailure`,
  `AssertionError`** — these are definition or graph breakage rather
  than runtime degradation. A broken definition cannot meaningfully
  trigger a fallback, and the fallback would probably sit in the same
  broken graph.

It fires for everything else, including `ProcessCrash`,
`WatchdogTimeout`, `HealthCheckFailure`, the startup failures, and
`RestartBudgetExhausted` on a non-Critical service.

For a **Critical** service exhausting its budget, the reboot takes
precedence and no fallback is started. The suppression keys on the cause
and the service's `ErrorControl` rather than on whether a reboot was
actually scheduled.

`OnFailure` is for graceful degradation — the main web interface fails,
so start a minimal emergency endpoint. It is not for monitoring or
alerting, which is eventd's job.

### 6.3.3.1 The loop guard

An `OnFailure` handler can fail and carry its own `OnFailure`, so a
misconfiguration where A's handler is B and B's is A could run forever.
peinit bounds the chain originating from one failure two ways: it tracks
the set of services already started as handlers for that failure and
will not start one already in the set, and it will not follow the chain
past a fixed depth of 16. When either trips, peinit records an
`on_failure.loop_suppressed` event naming which, and stops.

The chain is cleared when a handler reaches Completed, Inactive, Skipped
or Abandoned. A handler that starts and *stays running* keeps its entry,
so it continues to occupy a slot of that originating failure's budget.

## 6.3.4 The logging contract

Every state transition produces a record covering four things: what
failed, why it failed, what peinit did about it, and what the
administrator should do. Cryptic failure messages are a defect. A reboot
loop caused by a configuration error with an opaque message is the worst
outcome the system has, and the cause taxonomy exists so that the "why"
is never a guess.

---

# 6.4 Restart

_Peios / Advanced Peios / peinit / The State Machine_

> What happens when a restart-eligible cause occurs — the policies, backoff, the budget and when it resets, and exhaustion.

When a restart-eligible cause occurs, or a Simple clean exit produces
`CleanExitRestart`, peinit evaluates the restart policy. The outcome is
either Failed, or Backoff followed by Starting.

```
evaluate_restart(service, cause):
    // 1. Policy.
    if cause is never-restart:
        return STAY_FAILED
    if cause == CleanExitRestart and policy != Always:
        return INVALID_CAUSE_FOR_POLICY
    if policy == Never:
        return STAY_FAILED
    if policy == OnFailure:
        // A termination counts as success only when the cause is a
        // process exit whose code is in SuccessExitCodes. Only
        // ProcessCrash and CleanExitRestart carry an exit code, and
        // CleanExitRestart was handled above. Every other eligible
        // cause has no exit code and is always a failure here.
        if cause == ProcessCrash and exit_code in success_exit_codes:
            return STAY_FAILED
    // Always falls through unconditionally.

    // 2. Budget.
    if consecutive_failures >= restart_max_retries:
        cause = RestartBudgetExhausted
        if error_control == Critical:
            sync and reboot
        return STAY_FAILED

    // 3. Delay.
    delay = min(restart_delay << consecutive_failures, 60)

    // 4. Schedule.
    return RESTART_AFTER(delay)
```

`RESTART_AFTER` puts the service in Backoff for the delay; when it
elapses the service transitions to Starting and the ordinary activation
sequence begins, with its own fresh `StartTimeout`.

The exit code is available only when peinit observed a process exit. For
a Simple service that exits before signalling readiness the code is not
carried into the evaluation, so the `SuccessExitCodes` branch of the
`OnFailure` policy cannot apply and such a service is always restarted.

## 6.4.1 The policies

| Policy | Value | Behaviour |
|---|---|---|
| Never | 0 | Never restart. The service stays Failed. |
| OnFailure | 1 | Restart on a non-zero exit or a runtime failure. An exit matching `SuccessExitCodes` is not restarted. |
| Always | 2 | Restart on any failure regardless of exit code, and for a Simple service also on a successful clean exit. |

For a **Oneshot** with `RestartPolicy=Always`, a successful exit is not
restart-eligible. `RestartPolicy` governs the response to failures; a
Oneshot that succeeds has done its job. It goes to Completed — and then
Inactive without `RemainAfterExit` — whatever the policy says, and only
a non-zero exit reaches the restart evaluation at all. Timer triggers
are the mechanism for re-running a Oneshot on a schedule.

## 6.4.2 Backoff

The delay doubles on each consecutive failure, starting from
`RestartDelay` and capped at 60 seconds. The arithmetic is
overflow-safe, so a large `RestartDelay` or a long failure run saturates
at the cap rather than wrapping.

While a service is in Backoff it is down and does not satisfy
dependents. An explicit `start` creates or merges into a **deferred**
start operation, which honours the remaining delay rather than
short-circuiting it; a `stop` cancels the pending restart and takes the
service to Inactive.

## 6.4.3 The budget, and when it resets

`consecutive_failures` counts consecutive restart-eligible failures. It
resets to zero **only after the service has stayed Active for
`RestartWindow` seconds**. It is not a count of restarts within a
trailing window, and the difference is what the mechanism turns on.

peinit stamps the moment a service becomes dependent-satisfying, and
clears that stamp on any transition to a non-satisfying state — so a
crash restarts the clock. The reset fires when the stamp plus
`RestartWindow` is reached with the service still Active.

A service that recovers and stays Active longer than `RestartWindow`
between crashes therefore never exhausts its budget: each crash starts
from a counter of zero. Only failures recurring faster than the service
can sustain a window of health accumulate.

Two other events also zero the counter, both of which mean the service
is no longer in a failure run: a clean exit to Inactive, and an
administrative reset. An explicit stop while in Backoff does not — the
accrued failures are preserved.

Because the reset requires the service to be Active, a service that
happens to be Reloading when its window boundary passes misses that
reset and gets it on returning to Active.

## 6.4.4 Exhaustion

Once `RestartMaxRetries` restarts have happened without the service
sustaining a window of health, the next failure is not restarted: Failed
with cause `RestartBudgetExhausted`. peinit then applies `ErrorControl`:

- **Normal** — the service stays Failed.
- **Critical** — peinit syncs the filesystems and reboots immediately.
  The reboot takes precedence over `OnFailure`.

The reboot is driven from the paths that observe a terminal outcome for
a running service: the main job ending, a health check failing or timing
out, and the watchdog expiring. A budget exhausted purely by startup
failures — repeated `ReadinessTimeout`, repeated `PreHookFailure` — does
not reach one of those paths, so a Critical service that can never get
as far as running settles in Failed rather than rebooting, and its
`OnFailure` handler is suppressed as well.

---

# 6.5 Reload

_Peios / Advanced Peios / peinit / The State Machine_

> Telling a service to re-read its configuration without restarting — choosing the signal or command path, auto-detection, and interruptions.

Reload tells a service to re-read its configuration without restarting.
peinit issues the reload, moves the service to Reloading, and resolves
it one of three ways.

A failed reload never takes a running service out of Active. And reload
never gets stuck: every path has a timeout.

## 6.5.1 Choosing a path

`ExecReload` absent means SIGHUP to the main process. A `signal:<NAME>`
value means that signal instead. Anything else is a command, forked into
the service's `hooks/` sub-cgroup under the service's **own** identity —
never peinit's token, and `HookIdentity` does not apply.

## 6.5.2 The signal path

There is no command exit to observe, so completion is inferred from the
main process's own notifications.

```
start the detection window (2 seconds)

on READY=1, at any time:
    -> Active, "confirmed"

on RELOADING=1 within the window:
    cancel the window
    start the extended wait (StartTimeout)

on the window expiring with no RELOADING=1:
    -> Active, "advisory"

on the extended wait expiring with no READY=1:
    -> Active, "advisory"
```

The two-second window is a constant and is not configurable through the
registry. It is bounded from above by the operation's own deadline, so a
service whose `StartTimeout` is under two seconds gets the shorter of
the two.

The extended-wait expiry means the service announced a reload and never
finished one. The outcome is carried in the operation's result, which a
`wait=true` caller receives; a reload issued without waiting — the
default — resolves silently.

## 6.5.3 The command path

The command's exit gates failure; the main process's `READY=1` gates
confirmation.

```
on the command exiting non-zero:
    -> Active, "failed"

on the command exceeding StartTimeout:
    SIGKILL the hooks/ sub-cgroup, taking its descendants
    -> Active, "failed"

on the command exiting zero:
    -> Active, "confirmed" if the main process sent READY=1 during the
       reload, otherwise "advisory"
```

## 6.5.4 Auto-detection

The protocol needs no per-service configuration. A service that
implements the notification handshake — `RELOADING=1` then `READY=1` —
gets real lifecycle tracking. One that does not gets a brief Reloading
state that resolves itself when the detection window expires. Neither
has to declare which it is.

## 6.5.5 Interruptions

**The main process crashes while Reloading.** That is a `ProcessCrash`,
and the restart policy is consulted: Reloading to Backoff if a restart
is allowed and the budget holds, Reloading to Failed otherwise. Both
reload timers are cancelled and any in-flight reload command is killed.

This is a different event from an external reload *command* exiting
non-zero, which is the "failed" outcome above and leaves the main
process — and the service — running.

**A stop arrives while Reloading.** peinit cancels the reload
immediately: it drops the reload deadlines, kills any reload command's
cgroup, and sends SIGTERM in the same turn, without waiting out the
window or the extended wait. The service goes to Stopping and the reload
operation is aborted.

---

# 6.6 The Watchdog and Timeout Extension

_Peios / Advanced Peios / peinit / The State Machine_

> The two notification fields a running service uses to adjust the deadlines it is held to, their caps, and where they do not apply.

Two notification fields let a running service adjust the deadlines it is
held to. Both are authenticated exactly as any other notification
(§10.5), and both carry microseconds.

## 6.6.1 The watchdog

`WatchdogTimeout` sets the interval peinit expects `WATCHDOG=1` pings
at. Zero, the default, disables it. Missing a ping is a
`WatchdogTimeout` cause and takes the ordinary restart path.

A service may change the interval at runtime by sending
`WATCHDOG_USEC=<value>`:

- A value greater than zero updates the interval **and re-arms
  immediately** — the current timer is cancelled and a fresh one starts
  from the moment the message was received, rather than the new interval
  applying only from the next ping.
- A value of zero disables the watchdog entirely, equivalent to
  `WatchdogTimeout=0`.

The runtime value does not persist. On a restart the interval reverts to
the definition's `WatchdogTimeout` converted to microseconds, and if
that is zero the watchdog starts disabled whatever the previous
incarnation had set.

`WATCHDOG_USEC` is honoured only while the service is Active. A service
that sends it while still Starting — before its own `READY=1` — is
ignored and gets the definition's value.

> [!NOTE]
> Runtime watchdog updates suit a service whose phases have genuinely
> different latency. A database engine might want a tight five-second
> watchdog in normal operation and sixty seconds during a compaction
> pass. The service knows its own phases better than whoever wrote its
> definition does.

## 6.6.2 Timeout extension

A service may ask for more time during a start, stop or reload by
sending `EXTEND_TIMEOUT_USEC=<value>`.

peinit sets the current phase's deadline to expire that many
microseconds from now. The extension **replaces** the deadline rather
than adding to it — each message sets an absolute deadline computed from
its own arrival — and may be sent repeatedly.

Because it replaces, a small value shortens the remaining time rather
than being ignored, and a value of zero sets the deadline to now.

### 6.6.2.1 The caps

The extended deadline cannot exceed four times the phase's base timeout:

| Phase | Base | Ceiling |
|---|---|---|
| Starting | `StartTimeout` | `StartTimeout` × 4 |
| Stopping | `StopTimeout` | `StopTimeout` × 4 |
| Reloading | `StartTimeout` | `StartTimeout` × 4 |

A value beyond the cap is clamped, not rejected — the message succeeds
and the deadline becomes the maximum permitted. Because the cap is
anchored to when the operation started rather than to the previous
deadline, repeated messages cannot creep past it.

During shutdown an additional cap applies: the deadline cannot exceed
the time remaining in the global `ShutdownTimeout`, and where both caps
apply the stricter wins.

### 6.6.2.2 Where it does not apply

A message arriving while the service is in a non-transitional state —
Active, Completed, Failed — is ignored. There is no deadline to extend.

During shutdown the extension applies only to a service whose stop wave
has already begun. A service in a later wave, or one still winding down
a start or a reload when shutdown was requested, has no shutdown
deadline recorded yet and its extension request has no effect.

> [!NOTE]
> Timeout extension is for a service doing variable-duration work in a
> transition — replaying a write-ahead log on startup, say. It sends
> periodic messages to prove it is making progress; if it stops sending,
> the last deadline fires and peinit escalates as usual. The fourfold
> cap is what stops a buggy service extending forever.

---

# 7.1 Relationships

_Peios / Advanced Peios / peinit / Dependencies_

> The four relationship types — Requires, Wants, BindsTo and Conflicts — and what each means at start, at stop and on failure.

Four relationship types. Each says something different about how two
services interact at start, at stop, and on failure.

## 7.1.1 Requires

A hard dependency. If A `Requires` B:

- **Start.** B has to reach a dependent-satisfying state before A
  starts.
  If B is not running, peinit starts it with cause `DependencyStart`. If
  B enters Failed, A goes to Failed with cause `DependencyFailure`
  without attempting to start.
- **Stop.** Stopping B does not stop A. This is a start-ordering
  constraint, not a runtime coupling.
- **Runtime failure.** If B crashes while A is Active, A is unaffected
  and keeps running. B's own restart policy handles B.
- **Missing target.** A goes to Failed with `DependencyFailure`,
  detected at graph validation.

## 7.1.2 Wants

A soft dependency. If A `Wants` B, peinit starts B before A — with
cause `DependencyStart` — provided B exists and is not disabled. If B
fails to start, or does not exist at all, A starts anyway. There is no
stop or failure effect in either direction.

The waiting rule is where the difference from `Requires` actually lives:
a dependent blocked on a `Requires` target waits for it to reach a
*satisfying* state, while a dependent blocked on a `Wants` target waits
only for it to reach *any terminal* state, satisfying or not. That is
what makes `Wants` ordering rather than dependency — "start this first
if you can, but I will work without it".

## 7.1.3 BindsTo

A runtime coupling. If A `BindsTo` B:

- **Start.** Identical to `Requires`.
- **Stop.** If B stops for *any* reason — explicit stop, conflict,
  crash, shutdown — A stops too, transitioning to Stopping with cause
  `BindsToPropagation`.
- **Recovery.** When B returns to Active, peinit automatically restarts
  anything sitting in Failed with cause `BindsToPropagation` from B's
  stop. This is reactive rather than polled: peinit watches for the
  transition into Active from a non-satisfying state, and reacts to it.
  These restarts do **not** consume the restart budget — the dependent
  never failed on its own, it was stopped because its binding target
  went away.

`BindsTo` implies `Requires`. A definition may list both for clarity,
and if it does, the `BindsTo` semantics apply.

## 7.1.4 Conflicts

Mutual exclusion. Starting A while B is Active creates a stop operation
for B — source `ConflictResolution`, cause `ConflictEviction` — and A
does not start until B has left every state in which it could still be
running.

Conflicts are **symmetric**. If A declares `Conflicts = ["B"]`, starting
either one stops the other; B does not have to declare it reciprocally,
and peinit scans both a starting service's own conflicts and everything
that declares a conflict against it.

If both A and B are boot-triggered and they conflict, graph validation
detects an unresolvable conflict and fails both with `ValidationError`.

A missing conflict target is silently dropped — there is nothing to
conflict with.

> [!NOTE]
> `Conflicts` is for real mutual exclusion: two services binding the
> same port, or two implementations of one role where exactly one must
> run. It is not a way to solve resource contention that has a better
> answer.

## 7.1.5 Ordering and self-reference

Dependencies imply start ordering, and — except for `BindsTo` — imply
nothing about stopping. peinit starts dependencies before dependents;
during shutdown it reverses the same graph and stops dependents before
dependencies (§12.2), derived from the one graph rather than from a
separate stop-ordering configuration.

A service may not name itself in any dependency field. peinit rejects a
self-reference at graph validation as a `CycleDetected` failure, which
is what it is — a cycle of length one.

---

# 7.2 Graph Validation

_Peios / Advanced Peios / peinit / Dependencies_

> peinit validates the dependency graph before executing it — cycles, missing targets, and the errors and warnings it produces.

peinit validates the dependency graph before executing it. Validation
runs once per graph build — at boot, on an on-demand start's transitive
closure, and on a reload-config — and is never incremental.

## 7.2.1 Cycles

peinit topologically sorts the graph; if the sort fails, a cycle exists.
Detection returns **all** cycles rather than stopping at the first: each
detected cycle's members are removed and the search re-run until the
graph is clean.

Every service in a cycle is failed with cause `CycleDetected`, and the
cycle path is logged so an administrator can see what to break.

If any service in a cycle is Critical, peinit downgrades to Safe mode
(§2.6) rather than rebooting. The cycle is a configuration error, and
rebooting would find it again.

## 7.2.2 Missing targets

| Relationship | A missing target means |
|---|---|
| `Requires` | The dependent is failed with `DependencyFailure`. |
| `BindsTo` | The same — treated as a missing `Requires`. |
| `Wants` | Silently dropped. |
| `Conflicts` | Silently dropped. |

Detection is a Full-mode behaviour. In Safe mode a hard-dependency
target that is missing, disabled, or not Safe-mode-eligible does not
block the dependent, which is started with the dependency unmet.

## 7.2.3 Validation errors

A service that hits one is failed with cause `ValidationError` and never
started:

- The flap constraint,
  `HealthCheckRetries × HealthCheckInterval < RestartWindow` (§5.6).
- An invalid timer calendar expression (§9.1). This one is checked
  across every definition, not only those in the graph.
- Two boot-triggered services that conflict with each other. Both are
  failed. Safe mode applies if either is Critical.

## 7.2.4 Validation warnings

Logged, and do not prevent boot:

- A service using `Readiness=Alive` that something else depends on
  hard. `Alive` readiness means the process exists, which is no
  guarantee it is functional, so anything waiting on it is waiting on
  the wrong thing.

## 7.2.5 Multiple findings

A service can attract more than one finding in one pass — being both in
a cycle and missing a `Requires` target, say. The runtime state records
a single primary cause, chosen by precedence:

1. `CycleDetected`
2. `ValidationError`
3. `DependencyFailure`

The precedence affects only which cause is stored. Every other finding
for the service is retained beside it, in discovery order, and the
operation's failure message enumerates all of them:

```
CycleDetected: dependency cycle a -> b -> a (also: ValidationError:
health check interval exceeds the restart window) [2 findings]
```

The primary comes first and unqualified, so a service with one finding
reads exactly as it always did. A higher-precedence finding arriving
later demotes the previous primary rather than deleting it — breaking
the cycle should not be what it takes to discover the second fault.

Every finding is also emitted as its own `graph.validation_error` KMES
event, carrying `phase: "boot"`. The console lines are for whoever is
watching the boot; the events are the account that survives it.

`HardDependencyBlocked` — blocked *because a dependency is blocked* —
gets its own `finding` value rather than being reported as a missing
dependency, which would claim the target does not exist when it does.
It has no reload-path equivalent, because reload rejects wholesale
instead of propagating a block.

The reload path behaves differently, because its consequence is
different. Validation there accumulates every finding, encodes each as
its own event under `phase: "reload_config"`, and then rejects the
**entire reload** — the previous generation stays live and the findings
return to the caller (§10.4). Boot marks individual services and
continues; reload reports everything and changes nothing.

Both use the same event type deliberately: one consumer filter catches
validation problems in either regime, and `phase` says which.

---

# 7.3 Graph Execution

_Peios / Advanced Peios / peinit / Dependencies_

> Starting everything whose dependencies are satisfied and repeating until nothing more can start — contexts, failure propagation and shutdown ordering.

Executing a graph means starting the services whose dependencies are all
satisfied, and doing it again each time something becomes satisfying,
until nothing is left.

```
execute_graph(graph, max_parallel):
    ready = services with no unsatisfied dependencies
    in_flight = 0

    while ready is not empty or in_flight > 0:
        while ready is not empty and in_flight < max_parallel:
            begin_start(ready.dequeue())
            in_flight += 1

        match wait_for_event():
            ServiceSatisfied(s):
                in_flight -= 1
                for each dependent of s:
                    if all its dependencies are satisfied:
                        ready.enqueue(dependent)

            ServiceFailed(s):
                in_flight -= 1
                propagate_failure(s)
```

`MaxParallelStarts` bounds the concurrency, counted per context as the
members currently running.

## 7.3.1 Execution contexts

A graph execution is a retained object, not a transient loop. peinit
holds a **context** carrying its members, their dependencies and their
status, and there are two kinds: one boot context built from the boot
plan, and an on-demand context per explicit start, built from that
service's validated transitive closure.

Both use the same scheduler, the same satisfaction rules, the same
failure propagation and the same parallelism, but they are distinct
runtime objects — which matters because they can overlap.

An operation is associated with **every** context that created or
adopted it. One operation can belong to more than one active on-demand
context: two administrators starting different services that share a
dependency both end up merged into the same already-starting operation
for it, and both contexts need to hear how it turns out.

So when a pre-start outcome is terminal for an operation, peinit
dispatches the corresponding graph event once **per associated
context**. An operation with no associated context completes or fails
normally, and its waiters are notified, but no graph event is
dispatched.

A member whose dependent resolves without ever needing it is **pruned**
— a dormant sub-tree is cancelled rather than started, so an on-demand
start that turns out not to need half its closure does not start that
half.

Contexts are not retired. A context and its operation associations
persist for the lifetime of the process.

## 7.3.2 Failure propagation

When a service enters Failed during graph execution:

1. Everything that `Requires` or `BindsTo` it transitions to Failed with
   cause `DependencyFailure`.
2. Everything that `Wants` it is unaffected and starts normally.
3. Propagation is transitive: if A requires B and B requires C, and C
   fails, then B fails and then A fails.

## 7.3.3 On-demand start

Starting a service explicitly resolves its dependencies first:

1. Collect the transitive `Requires` and `BindsTo` closure.
2. Collect the transitive `Wants` closure, best-effort.
3. Validate the sub-graph — cycles, missing targets.
4. Resolve conflicts, stopping whatever has to stop.
5. Start the sub-graph with the same parallel algorithm. Dependencies
   started this way carry cause `DependencyStart` and operation source
   `DependencyPropagation`.

A service already in a satisfying state — Active, Completed or Skipped —
is not restarted. Its dependency is already met.

The on-demand path treats a **disabled** hard-dependency target
differently from the boot path: where boot blocks the dependent, an
on-demand start includes the disabled target and starts it.

## 7.3.4 Shutdown ordering

Shutdown reverses the graph: services with no dependents stop first,
services that everything depends on stop last, derived by reverse
topological sort from the same edges.

Only hard dependencies — `Requires` and `BindsTo` — contribute to the
ordering. A `Wants` dependent may therefore be stopped after its target.

The ordering is entirely emergent from what the definitions declare.
There is no floor and no pinning, so where the TCB services end up in
the wave order depends on their declared dependencies being right.

---

# 7.4 The Boot and On-Demand Paths

_Peios / Advanced Peios / peinit / Dependencies_

> The two ways a graph gets built and how they differ beyond scope — including the three places a bad definition can surface.

The two ways a graph gets built differ in more than scope, and the
differences are worth having in one place.

| | Boot | On-demand |
|---|---|---|
| Members | Every boot-triggered root plus its closure | One requested service plus its closure |
| A missing hard-dependency target | Blocks the dependent, in Full mode only | Blocks the dependent |
| A disabled hard-dependency target | Blocks the dependent | Starts it |
| A validation finding | The service is failed, others continue | The start fails |
| Multiple findings on one service | Only the highest-precedence one is reported | — |
| Conflicts | Two boot-triggered conflicting services fail both | The conflicting service is evicted |
| Context | One boot context | One context per explicit start |
| Failure of the whole build | Recovery mode | An error to the caller |

## 7.4.1 Where a definition error lands

The three places a bad definition can be caught behave differently, and
which one catches it depends on what kind of wrong it is:

- **Decoding.** A definition that does not parse — an invalid name, a
  malformed trigger, an unclosed quote, a `registry:` check naming an
  uncacheable key, a duplicate field — is caught when the registry is
  read. At boot it fails that service with `ValidationError` and the
  rest continue; on a reload it rejects the whole reload (§3.2).
- **Graph validation.** A definition that parses but does not fit —
  a cycle, a missing target, a flap-constraint violation, an invalid
  calendar expression — is caught here, and fails that service at boot
  or the whole reload on a reload-config (§7.2).
- **Start.** A definition that parses and fits but whose preconditions
  do not hold — a failed assert, an unresolvable identity, a missing
  binary — is caught when the service actually starts, and fails that
  activation (§5.2, §5.3).

The dividing line between the first two is whether the problem is
visible in one definition on its own. Decoding sees one key at a time;
validation is the first place that can see two definitions together.

---

# 8.1 Jobs

_Peios / Advanced Peios / peinit / Jobs and Operations_

> A job is one supervised process execution, and every fork peinit performs is one — its lifecycle, fields, retention and ownership.

A job is one supervised process execution. Every fork peinit performs is
a job: a service's main binary, a pre-exec hook, a post-exec hook, a
reload command, a health check invocation, an ad-hoc submission.

Jobs are the observable unit of *what actually ran*. Services are
definitions carrying identity, policy and configuration; jobs are
instances. A restart creates a new job.

## 8.1.1 Lifecycle

```
Created --> Running --> Completed
   |           |
   |           +------> Failed
   |           |
   |           +------> Abandoned
   +------------------> Failed
```

| State | Meaning |
|---|---|
| Created | The job object exists but exec has not succeeded. The process may not have been forked, or it may be in pending post-fork setup with the error pipe unresolved. |
| Running | Exec succeeded and the process is alive. |
| Completed | The process exited successfully — code 0, or one in `SuccessExitCodes`. |
| Failed | The process failed, or peinit classified the job failed before the fork because parent setup failed. |
| Abandoned | The process survived SIGKILL. |

Job states are simpler than service states because they describe a
process rather than a policy. A service has Starting, Reloading and
Backoff because those are decisions; a job is running, or it finished,
or it is stuck.

## 8.1.2 Fields

```
Job {
    id:                 GUID       // UUIDv7
    service:            string?    // null for ad-hoc
    job_type:           enum       // ServiceMain, PreExecHook, PostExecHook,
                                   // ReloadHook, HealthCheck, AdHoc
    state:              enum
    pid:                u32?
    pidfd:              fd?
    resolved_identity:  string     // the resolved service, hook or submitter identity
    token_summary:      object     // the resulting SID, groups, privileges
    image_path:         string
    arguments:          string[]
    created_at_ns:      u64
    started_at_ns:      u64?
    ended_at_ns:        u64?
    exit_code:          i32?
    exit_signal:        i32?
    failure_cause:      string?
    cgroup_id:          string
    cgroup_generation:  u32
    activation_generation: u32
    operation_id:       GUID?      // null for ad-hoc
    hook_index:         u32?
}
```

The rules that govern when the nullable fields are populated are what
make a job record trustworthy:

- `id` is assigned **before** the fork, so a job that never forks still
  has an identity.
- `pid` and `pidfd` land on the record only once exec success is
  confirmed by EOF on the error pipe. Until then they are held in
  pending setup state and the job is Created.
- A setup failure takes the job straight from Created to Failed.
  `ended_at_ns` records the **classification** time, `failure_cause`
  records what went wrong, and `pid`, `pidfd`, `started_at_ns`,
  `exit_code` and `exit_signal` all stay null. There was no process to
  have a PID or an exit status.
- `exit_code` and `exit_signal` are populated only when peinit observed
  an exit — never both, since a process either exits or is killed.
- For an Abandoned job, `ended_at_ns` records when peinit stopped
  supervising, and the exit fields stay null. Nothing exited.

`resolved_identity` is the identity *string* — `SYSTEM`,
`LocalService`, a SID — that was resolved for the execution.
`token_summary` is what the resulting token actually contains. They are
separate because they can differ, and the `identity` field exposed in
status views and job events is the former.

## 8.1.3 Retention

peinit tracks active jobs in memory. When a job reaches a terminal state
it emits a structured event carrying the full record and then **drops**
the job. There is no job history in peinit, and no structure that could
hold one.

eventd is the historian. It consumes those events from the KMES kernel
ring buffer, and a query for a service's past jobs is a query to eventd.

## 8.1.4 Ownership

| Concern | Owner |
|---|---|
| Restart policy, dependencies, health check schedule, `ErrorControl` | Service |
| Current state — Active, Failed, … | Service |
| PID, pidfd, exit code, exit signal | Job |
| Execution timestamps | Job |
| Identity and token | Job |
| cgroup assignment | Job |
| Log correlation | Job |

A service tracks its current main job's identifier, and a status query
returns it.

---

# 8.2 Operations

_Peios / Advanced Peios / peinit / Jobs and Operations_

> A requested state machine action as a first-class object — every control command creates one rather than mutating state directly.

An operation is a requested state machine action on a service, as a
first-class object. Control commands do not mutate state directly: every
one creates an operation that is validated, queued, resolved against
whatever else is in flight, and executed by the event loop.

Operations exist because peinit serves concurrent callers —
administrative tools, automated triggers, other services. Without them,
two commands arriving together collide with whatever behaviour falls
out; with them, the resolution is explicit and observable.

## 8.2.1 Lifecycle

```
Pending --> Running --> Completed
  |           |
  +-> Merged  +-------> Failed
  |           |
  +-> Cancelled +-----> Aborted
  |
  +-> Failed
```

| State | Meaning |
|---|---|
| Pending | Validated and queued, waiting on a precondition. |
| Running | Executing. |
| Completed | The goal was reached. Start: Active for Simple, Completed or Inactive for Oneshot. Stop: the service is no longer running. Reload: the reload resolved. |
| Failed | The goal was not reached — or the operation's maximum lifetime expired while it was still Pending. |
| Merged | Merged into an existing identical operation, whose identifier is recorded. |
| Cancelled | Terminated while Pending. It never executed. |
| Aborted | Terminated while Running. |

Cancelled and Aborted are the same idea at different points: never ran
versus was running. Why it happened is a property of the event, not of
the state.

## 8.2.2 Fields

```
Operation {
    id:               GUID
    operation_type:   enum    // Start, Stop, Restart, Reload, Reset
    service:          string
    state:            enum
    created_at_ns:    u64
    started_at_ns:    u64?
    completed_at_ns:  u64?
    source:           enum
    caller:           token_summary?   // admin-initiated only
    result:           string?
    merged_into:      GUID?
}
```

## 8.2.3 Sources

Why peinit created the operation:

| Source | Meaning |
|---|---|
| `Admin` | A control client asked for it. |
| `Boot` | The Phase 2 boot plan. |
| `Shutdown` | The shutdown lifecycle. |
| `DependencyPropagation` | A start operation created one for an unsatisfied dependency. |
| `RestartPolicy` | A restart policy generated a start. |
| `Timer` | A timer trigger fired. |
| `BindsToRecovery` | A bound target returned to Active. |
| `BindsToPropagation` | A bound target stopped. |
| `ConflictResolution` | A conflict evicted the running service. |
| `OnFailure` | A failed service's fallback handler. |

`Shutdown` is declared and labelled but not currently produced: shutdown
transitions services and signals them directly, without creating
operations for the stops (§12.2).

## 8.2.4 The types

**Start** creates a job for the target. Unsatisfied dependencies produce
their own start operations with source `DependencyPropagation`. It
completes when the service reaches Active, Completed or Inactive as
appropriate, or Skipped when pre-start conditions do not hold.

**Stop** sends SIGTERM, arms `StopTimeout`, escalates to SIGKILL. It
completes when the service reaches Inactive, or Failed after a conflict
eviction or bound-dependency propagation.

**Restart** is a stop then a start, tracked under one identifier across
both phases, and the type stays `Restart` throughout for observability.

**Reload** issues the reload command or signal (§6.5) and completes when
the reload resolves. Unlike the other lifecycle commands it defaults to
not waiting — the caller gets the identifier immediately.

**Reset** clears Failed, Abandoned or Skipped, taking the service to
Inactive. It is synchronous.

## 8.2.5 Timeouts

A start, reload or reset inherits the target's `StartTimeout` as its
maximum lifetime; a stop inherits `StopTimeout`. A restart has two legs,
each enforced against its own timeout, with the sum as the overall
lifetime.

**The clock starts at creation, including queue time.** From the
caller's point of view they have been waiting since they sent the
command, not since peinit got round to it. A start that sits Pending
behind a stop for longer than `StartTimeout` fails without ever running.

## 8.2.6 Retention

Pending and Running operations are held in memory. A terminal operation
is emitted as an event and dropped after a grace period of 60 seconds —
long enough for a polling client to collect the result.

peinit keeps no operation history, for the same reason it keeps no job
history. eventd is the historian.

---

# 8.3 Conflict Resolution

_Peios / Advanced Peios / peinit / Jobs and Operations_

> Resolving a new operation against one already pending or running — merging, cross-type conflicts, and the principles behind both.

When a new operation is requested and one for the same service is
already Pending or Running, peinit resolves the two.

## 8.3.1 Merging

An operation of the same type merges. The new caller receives the
**existing** operation's identifier, and from their point of view their
request is in progress — they neither know nor need to know that it
merged.

| Existing | New | Resolution |
|---|---|---|
| Start | Start | Merge |
| Stop | Stop | Merge |
| Reload | Reload | Merge |
| Restart | Start | Merge — a restart already includes a start |

Restart is not mergeable with itself. A second restart while one is in
progress is queued.

## 8.3.2 Cross-type

| Existing | New | Resolution |
|---|---|---|
| Start (Pending) | Stop | Cancel the start, create the stop |
| Start (Running) | Stop | Abort the start, create the stop |
| Start (Pending) | Restart | Cancel the start, queue the restart |
| Start (Running) | Restart | Queue the restart |
| Stop (either) | Start | Queue the start |
| Stop (either) | Restart | Queue the restart |
| Restart (Pending) | Stop | Cancel the restart, create the stop |
| Restart (Running) | Stop | Abort the restart, create the stop |
| Restart (either) | Restart | Queue |
| Reload (Pending) | Stop | Cancel the reload, create the stop |
| Reload (Running) | Stop | Abort the reload, create the stop |
| Reload (Pending) | Restart | Cancel the reload, create the restart |
| Reload (Running) | Restart | Abort the reload, create the restart |
| Anything (either) | Reset | Reject |

Combinations outside this table are rejected: a new Reload while a
Start, Stop or Restart is active, and a new Start while a Reload is
active.

## 8.3.3 The principles

1. **Stop wins over start.** An explicit stop always takes priority. The
   administrator said stop, so stop; a queued start can follow.
2. **Later supersedes earlier.** Start then immediately stop means the
   stop wins and the start is cancelled, recorded as superseded.
3. **Merging is transparent.** The merged caller gets the original
   identifier and blocks on the original operation's outcome.

Reset is rejected outright while anything is in flight, because reset
means "clear a terminal state" and nothing in flight has one.

## 8.3.4 Dependency propagation

When a start executes against a service with unsatisfied dependencies,
peinit creates start operations for them:

- **`Requires`** — source `DependencyPropagation`. If one fails, the
  parent operation fails with `DependencyFailure`.
- **`Wants`** — source `DependencyPropagation`. If one fails, the parent
  continues.
- **`BindsTo` recovery** — source `BindsToRecovery`, created when a
  bound target returns to Active, for dependents sitting in Failed with
  cause `BindsToPropagation`.

Dependency-created operations follow the same resolution rules as
administrator-created ones. If a dependency is already starting because
something else also depends on it, the operations merge — and both
graph execution contexts are then associated with the one operation
(§7.3).

`BindsToRecovery` restarts are not subject to the restart budget. They
are created because a dependency returned, not because anything failed.

## 8.3.5 Restart policy and timers

A restart-eligible failure creates a start operation with source
`RestartPolicy` once the backoff delay elapses. It goes through the
ordinary validation and resolution: if an administrator has already sent
a stop, or the budget is exhausted, it is rejected.

A timer firing creates an operation based on the service's current
state:

| Type | State | Action |
|---|---|---|
| Oneshot | Inactive, Completed, Failed | Create a start, source `Timer`. |
| Oneshot | Active, Starting | Set the pending flag. One catch-up run, no operation. |
| Simple | Inactive, Failed | Create a start, source `Timer`. |
| Simple | Active, Starting | No-op. The firing is recorded. |

The Oneshot catch-up creates its start when the current run completes.
Multiple missed firings collapse into one pending run.

## 8.3.6 Boot and shutdown are not operations

Boot and shutdown are modes peinit enters, which then generate
per-service operations. There is no "shutdown operation" to observe or
cancel. Boot-generated starts use source `Boot`.

---

# 8.4 Event Emission

_Peios / Advanced Peios / peinit / Jobs and Operations_

> The structured event peinit emits at every job and operation transition, plus its own audit and graph records.

peinit emits a structured event at every job and operation lifecycle
transition, and for its own audit records. All of them go into the KMES
kernel ring buffer through `kmes_emit` and `kmes_emit_batch`, encoded as
msgpack per the KMES event-record format (Peios Kernel TRM §2).

**There is no event socket.** Structured events are not sent to eventd
over any connection. eventd consumes them from the ring buffer, which is
why they survive eventd being down, being restarted, or not existing
yet. The only thing peinit sends eventd over a socket is service output
(§11.4), which is a different path with different guarantees.

## 8.4.1 Job events

**`job.created`** — the job object exists. Carries the job identifier,
service name, type, image path, identity and operation identifier.

**`job.started`** — exec succeeded. Carries the job identifier, PID and
cgroup path.

**`job.ended`** — the process exited or was killed. Carries the job
identifier, final state, exit code or signal, duration and failure
cause.

The event type is the dotted string; the fields form the msgpack
payload. The payloads are supersets of the summaries above — `job.ended`
in particular carries the whole record.

## 8.4.2 Operation events

Every operation event carries: `operation_id`, `type`, `service`,
`source`, `caller` — null for a lifecycle-generated operation — and
`state` after the transition.

| Event | Adds |
|---|---|
| `operation.requested` | — |
| `operation.started` | — |
| `operation.completed` | `duration_ns`, `result` |
| `operation.failed` | `duration_ns`, `failure_reason` |
| `operation.cancelled` | `reason` |
| `operation.merged` | `merged_into` |
| `operation.aborted` | `duration_ns`, `reason` |

`duration_ns` is measured from **creation**, not from the start of
execution — the same reasoning as the operation timeout. What a caller
waited is what matters, and queue time is part of it.

The same field appears under three names across the two surfaces:
`failure_reason` on `operation.failed`, `reason` on
`operation.cancelled` and `operation.aborted`, and `error` in the
control interface's operation view (PSPU §4).

## 8.4.3 Ordering

When one runtime step produces several lifecycle events, peinit emits
them in causal order before committing the retained state for that step.
For a terminal pre-start graph dispatch, the terminal event for the
operation whose outcome satisfied or failed the graph input precedes the
events for the operations that dispatch releases, which preserve graph
dispatch order.

Every operation in a graph context is requested when the context is
built rather than when its turn comes, so what a release emits is
`operation.started`.

## 8.4.4 Audit and graph events

peinit's own audit records go through the same path: `access.denied` for
a refused control command, with the caller's SID, the target, the
requested right by name and the access bits requested and granted;
`on_failure.loop_suppressed` when the fallback chain guard trips;
`graph.validation_error` and `graph.validation_warning` for validation
findings; `notify.rejected` for an unauthenticated notification;
`fd_store.rejected` for a refused descriptor; `notify.status`,
`notify.errno` and `notify.exit_status` for the three event-emitting
notification fields; `cgroup.leaked` the first time a sub-cgroup is found
still populated after its post-kill deadline (§5.7); and
`graph.operation_terminal` for a graph member's terminal outcome.

Audit records are events rather than logs, and that distinction is the
point: the ring buffer persists from the moment PKM loads, so an access
denial during Phase 1 is captured before the registry exists, let alone
eventd.

---

# 8.5 Ad-Hoc Jobs

_Peios / Advanced Peios / peinit / Jobs and Operations_

> An arbitrary supervised process submitted by a service on behalf of its client — the delegation problem it solves, and its identity and lifecycle.

An ad-hoc job is an arbitrary supervised process submitted by a service
on behalf of its own client. It has no persistent definition: it runs
once, reports, and is cleaned up.

## 8.5.1 The delegation problem

A service — a privileged action broker, say — wants peinit to run a
process as one of its users. It has impersonated that user's token, but
KACS will not let it forward that token over IPC without
Delegation-level impersonation. Fork inheritance works, because the
kernel copies the token naturally, but then the service has to supervise
the process itself, which defeats the point of having a service manager.

JFS — the Job Forwarding Subsystem — is the kernel's answer. It captures
the caller's effective token and delivers it, with a job definition, to
whatever holds `/dev/jfs` open. peinit is the consumer, and JFS is a
generic primitive rather than something built for peinit.

peinit opens `/dev/jfs` during Phase 1 infrastructure setup and adds the
descriptor to its event loop. If nothing has the device open, a
submitter's syscall returns `ENODEV`.

## 8.5.2 The shape of a request

```
handle_jfs_request(request):
    (job_definition, token_fd) = read from /dev/jfs
    validate the image path, arguments, working directory
    job = Job { id: new_guid(), service: null, type: AdHoc,
                state: Created, token_summary: summarise(token_fd), ... }
    write job.id back to /dev/jfs        // unblocks the caller
    fork, install token_fd on the child, exec
    emit job.created, job.started, job.ended
```

## 8.5.3 The definition

A subset of the service definition's fields, arriving as structured data
rather than as registry values:

| Field | Required | Meaning |
|---|---|---|
| ImagePath | yes | The binary to execute. |
| Arguments | no | Its arguments. |
| Environment | no | Additional variables, as a map rather than `KEY=VALUE` strings. |
| Timeout | no | Maximum runtime in seconds. 0 means no limit. |
| WorkingDirectory | no | Defaults to `/`. |
| Description | no | For logs. |

The service-level fields deliberately absent are the policy ones:
`RestartPolicy` and its parameters, the four dependency fields,
`HealthCheck`, `WatchdogTimeout`, `ErrorControl`, `SafeMode` and
`Triggers`. Those belong to a persistent definition; an ad-hoc job runs
once.

## 8.5.4 Identity

The job runs with the token JFS captured — the caller's effective
identity at the moment of the syscall. An impersonating caller produces
a job running as the impersonated user; a caller using its own primary
token produces a job running as itself.

**There is no identity field.** A submitter cannot name an arbitrary
identity, only pass through the one it already holds. That is what stops
the mechanism being an escalation: a service cannot create jobs as
principals it could not already act as.

## 8.5.5 Lifecycle

1. Forked with the JFS-provided token.
2. Runs in its own cgroup under `/sys/fs/cgroup/peinit/`, the id derived
   from the job's GUID rather than from a service name.
3. Output routed to eventd, tagged with the job's GUID.
4. On exit: emit `job.ended`, clean up the cgroup, drop the job.
5. No restart, no dependencies, no health checks.

Exceeding `Timeout` sends SIGTERM, waits the schema default
`StopTimeout` of 10 seconds, then SIGKILL — the same escalation as a
service stop.

Ad-hoc jobs bypass the operations model entirely. A JFS request creates
a job directly, because there is no service to start and therefore no
state machine action to represent:

```
Admin --> start command --> Operation --> peinit forks --> Job
Broker --> JFS request  -->              peinit forks --> Job
Timer --> timerfd fires --> Operation --> peinit forks --> Job
```

## 8.5.6 The current state of JFS

JFS does not exist in the kernel. There is no `/dev/jfs` device, no
request encoding, and no mechanism for delivering a captured token
descriptor across a device read.

peinit's side is built up to that boundary and stops there. Phase 1
opens `/dev/jfs` if it is present, and a failure to open is a warning
that does not affect the boot. The descriptor is registered with the
event loop, and on the first readable event peinit reads nothing,
unregisters the descriptor and permanently disables the source.

The job machinery for ad-hoc jobs exists as far as the record: the job
type, the GUID-derived cgroup id, and a constructor. There is no launch
path — an ad-hoc job cannot currently be started — and the definition
fields above have no decoder to arrive through, so `Timeout`,
`Environment` and `WorkingDirectory` have nowhere to come from.

The byte-level `/dev/jfs` interface belongs to JFS rather than to
peinit, and this chapter describes the shape of the consumption
protocol rather than its encoding.

---

# 8.6 What a Caller Sees

_Peios / Advanced Peios / peinit / Jobs and Operations_

> What the job and operation model looks like from outside — identifiers, waiting, and why merging is invisible to the caller.

Operations are how a caller observes something taking effect. This
article covers what the model looks like from the outside; the wire
contract itself is PSPU §4.

## 8.6.1 Every lifecycle command returns an identifier

A command that creates, merges into, queues, cancels, clears or executes
an operation returns that operation's identifier. A caller can then poll
it, or block on it.

Two cases return no identifier, because no operation exists: a command
whose target is already in the state it asks for, and one that has no
effect at all — a stop on an Inactive service. Both return the service's
status instead of an acknowledgement, which is the honest answer.

Commands that never create an operation — `status`, `list`,
`operation-status` — have their own shapes.

## 8.6.2 Waiting

Lifecycle commands block by default until the operation is terminal. The
exception is `reload`, which returns immediately unless asked otherwise,
because a reload's outcome is often advisory and a caller usually wants
the identifier rather than the wait.

A waiting connection is not idle and is never closed by the idle
timeout, however long the operation runs. It is bounded by the
operation's own lifetime instead.

## 8.6.3 Merging is invisible

A caller whose command merged receives the surviving operation's
identifier and blocks on that operation's outcome. Nothing tells them
they merged, because there is nothing they could usefully do about it.
The consequence worth knowing is that the identifier a caller gets back
may be older than their request, and its `created_at` will be earlier
than when they sent it — which is exactly right, because that is when
the work they are waiting on actually began.

## 8.6.4 What an operation's result carries

A completed operation carries the resulting service state. A failed one
carries the failure reason. A merged one carries the survivor's
identifier. A cancelled or aborted one carries why.

For a reload, the result also determines the reload's *mode* — whether
the service confirmed the reload with a `READY=1`, whether peinit is
merely assuming it happened, or whether it outright failed.

---

# 9.1 Calendar Expressions

_Peios / Advanced Peios / peinit / Timers_

> The OnCalendar grammar peinit parses — component syntax, named shortcuts, timezones, daylight saving and precision.

A timer schedule is a calendar expression in systemd's `OnCalendar`
format. The grammar below is what peinit parses.

```
DayOfWeek Year-Month-Day Hour:Minute:Second Timezone
```

Every field is optional, and which fields are present is worked out
positionally from their shape.

| Field | Form | Default if omitted |
|---|---|---|
| DayOfWeek | weekday names | any day |
| Date | `Year-Month-Day` | `*-*-*` |
| Time | `Hour:Minute:Second` | `00:00:00` |
| Second | the `:Second` of Time | `:00` |
| Timezone | an IANA name | system-local |

A time-only expression such as `02:00:00` implies the date `*-*-*`.
`Hour:Minute` alone defaults the seconds to `00`.

Years range 0–9999, months 1–12, days 1–31.

## 9.1.1 Component syntax

Every numeric component, and the weekday, accepts:

- **Wildcard** — `*` matches anything.
- **List** — comma-separated values: `1,15`.
- **Range** — two values around `..`, inclusive: `Mon..Fri`, `8..17`.
  A reversed range is a parse error rather than a wrap-around.
- **Repetition** — a value or range suffixed with `/` and a step.
  `value/step` matches the value and every multiple of the step above
  it, so `0/15` in the minute field is 0, 15, 30 and 45.
  `start..end/step` walks from start to end inclusive.

Weekdays are English names, case-insensitive, abbreviated or full, and
accept lists and ranges. `Mon` and `Monday` are the same day, as are
`Tue`, `Tues` and `Tuesday`.

## 9.1.2 Last day of the month

A `~` in place of the `-` between Month and Day counts the day from the
**end** of the month: `~01` is the last day, `~02` the second-to-last,
and so on. `*-*~01` is the last day of every month; `*-02~03` is the
third-to-last day of February.

Repetition combines with it, and steps in the day-of-month direction —
which means it walks the offset *downwards*, towards the end of the
month. `Mon *-05~07/1` covers the last seven days of May, and combined
with `Mon` resolves to exactly one day: the last Monday in May.

Wildcards, ranges and lists are also accepted after `~`, so `~*` matches
every day and `~03..07` the third- to seventh-to-last.

## 9.1.3 Named shortcuts

| Shortcut | Equivalent |
|---|---|
| `minutely` | `*-*-* *:*:00` |
| `hourly` | `*-*-* *:00:00` |
| `daily` | `*-*-* 00:00:00` |
| `weekly` | `Mon *-*-* 00:00:00` |
| `monthly` | `*-*-01 00:00:00` |
| `quarterly` | `*-01,04,07,10-01 00:00:00` |
| `semiannually` | `*-01,07-01 00:00:00` |
| `yearly`, `annually` | `*-01-01 00:00:00` |

Shortcut names are case-insensitive and accept a trailing timezone, so
`daily UTC` is valid.

## 9.1.4 Timezones

Timezone specifiers are IANA database names — `Europe/London`,
`US/Eastern`, `UTC`. An expression with no timezone is interpreted in
system-local time.

A name is validated when the expression is parsed and resolved again at
evaluation. An unrecognised zone is a hard parse error, not a silent
fallback to UTC.

## 9.1.5 Daylight saving

- **Spring forward**, where the clock skips an hour: a scheduled time
  falling inside the skipped interval does not fire. peinit moves on to
  the next scheduled second, then the next date. A schedule of
  `*-03-31 01:30 Europe/London` skips 2024 entirely.
- **Fall back**, where an hour repeats: a scheduled time inside the
  repeated interval fires exactly once, on the **first** occurrence.
  On re-arming, the same civil time resolves to the same instant, which
  is not later than the firing that just happened, so the timer advances
  to the next day rather than firing again.

## 9.1.6 Precision

Second-level. Unlike systemd, peinit does not accept fractional seconds
— service scheduling has no use for finer granularity, and dropping it
keeps the parser simpler. A fraction anywhere in the time component is a
parse error.

## 9.1.7 Examples

| Expression | Meaning |
|---|---|
| `*-*-* 02:00:00` | Every day at 2am, system-local. |
| `Mon *-*-* 00:00:00` | Every Monday at midnight. |
| `*-*-1,15 12:00:00` | The 1st and 15th, at noon. |
| `*-*~01 00:00:00` | The last day of each month, at midnight. |
| `Mon..Fri *-*-* 09:00:00` | Weekdays at 9am. |
| `*-*-* *:00/15:00` | Every fifteen minutes. |
| `*-*-* 02:00:00 Europe/London` | Every day at 2am London time. |

> [!NOTE]
> An expression that parses but can never match — `*-02-30`, or a fixed
> year already past — is not rejected at parse time. Computing its next
> occurrence walks forward a day at a time to the year 9999 before
> concluding there isn't one.

---

# 9.2 Evaluation and Arming

_Peios / Advanced Peios / peinit / Timers_

> A timer is a trigger rather than a service type — how one is armed, how it fires, and what multiple triggers do.

A timer is a trigger, not a service type. A service with a
`timer:<schedule>` trigger is an ordinary Simple or Oneshot service that
peinit starts on a schedule.

## 9.2.1 Arming

At boot, once the service graph is loaded, and whenever timer
configuration changes, peinit computes the next firing time for every
active trigger and arms a timerfd for it. Each trigger gets its own
descriptor and its own computation.

A disabled service gets neither a registration nor a firing.

A schedule that fails to parse, or whose next occurrence cannot be
computed, fails that trigger. Every other timer arms normally, and what
did not arm is reported to the console. This matches how graph
validation already treats an invalid schedule (§7.4), so the outcome no
longer depends on which of the two caught it.

The next-occurrence search looks ten years ahead and then gives up. A
schedule can parse and still match nothing — `*-02-30`, or a fixed year
already past — and the horizon turns that into a prompt error against
the one service rather than a very long walk. Ten years clears the
sparsest schedule that is genuinely meaningful: `*-02-29` skips a
century year not divisible by 400, so it can run eight years dry.

## 9.2.2 Firing

```
handle_timer(service, trigger):
    // 1. Decide what the firing means, from the service's state.
    match (service.type, service.state):
        (Oneshot, Active | Starting):
            service.pending_timer = true      // at most one
        (Simple,  Active | Starting):
            record the firing; no action
        (_, Inactive | Completed | Failed):
            create_operation(Start, service, source = Timer)

    // 2. Record when it fired.
    write the last-run timestamp to the registry   // asynchronously

    // 3. Re-arm.
    next = next_occurrence(trigger.schedule, now) + random(0, TimerJitter)
    arm an absolute CLOCK_REALTIME timerfd for next
```

Every other state — Backoff, Stopping, Reloading, Abandoned, Skipped —
records the firing and does nothing.

The last-run write happens in a forked child so that the event loop
never waits on the registry. The parent returns immediately, and the
child is reaped as an untracked orphan; a failed write is visible only
as that child's exit status.

## 9.2.3 Oneshot pending runs

A Oneshot that fires while it is already running sets a flag rather than
queueing an operation. When it next reaches Inactive or Completed,
peinit immediately creates a start operation and clears the flag.

Multiple firings during one run collapse into a single pending run.
There is no queue, and the flag is per service rather than per trigger —
a service with three timers that all fire during one long run still gets
exactly one catch-up.

## 9.2.4 Multiple triggers

Triggers on one service are independent: each has its own timerfd, its
own next-firing computation, and its own last-run history. Only the
Oneshot pending flag is shared.

---

# 9.3 Persistence

_Peios / Advanced Peios / peinit / Timers_

> Where last-run history lives in the registry, how a missed firing is caught up after a reboot, and when the timestamp is written.

`TimerPersistent`, on by default, controls whether a run missed across a
reboot is caught up.

## 9.3.1 Where history lives

Last-run timestamps are `REG_QWORD` values in the registry, written
after a firing.

A service with a **single** timer trigger stores its timestamp as
`LastTimerRun` on the service's own key:

```
Machine\System\Services\<name>\LastTimerRun
```

A service with **multiple** triggers stores one per trigger under a
subkey, named by the schedule string:

```
Machine\System\Services\<name>\TimerState\<encoded-schedule>
```

A schedule contains characters — spaces, `:`, `*` — that are not valid
LCS value names, so the name is the schedule with every character
outside `[A-Za-z0-9._-]` percent-encoded, with uppercase hex digits.
This is the same encoding used for cgroup ids (§5.1). The schedule
`*-*-* 02:00:00` is stored as:

```
%2A-%2A-%2A%2002%3A00%3A00
```

Two identical schedule strings on one service encode to the same name
and therefore share one timestamp.

Timer firings are infrequent, so the write cost is negligible.

## 9.3.2 Catching up

On boot, for each persistent trigger:

1. Read the last-run timestamp.
2. Compute the next scheduled firing after it.
3. If that time has already passed, at least one run was missed: fire
   once, immediately.
4. Compute the next future occurrence normally.

Catch-up is always a **single** run however many were missed. A daily
timer that missed five days fires once on the next boot, not five times.

A trigger with no history at all is treated the same way, so its first
boot produces one catch-up firing.

`TimerPersistent=0` ignores history entirely — peinit does not even read
the registry for that trigger, and computes the next occurrence from
now.

## 9.3.3 When the timestamp is written

The timestamp is written after the timer fires and the start is
*initiated*, not after the service finishes. A service that crashes
mid-run is not re-triggered on the next boot: the run was attempted, not
missed.

A configuration reload re-arms every timer from the current time with no
catch-up, whatever `TimerPersistent` says. History is consulted at boot
only.

> [!NOTE]
> Timestamps for a multi-trigger service are keyed by schedule string,
> so changing a schedule orphans its history and the new schedule
> produces one spurious catch-up run. A stable trigger identifier would
> fix it; schedule changes are rare and one extra run is the whole cost.
> A single-trigger service is unaffected, since its timestamp lives at a
> fixed name.

---

# 9.4 Jitter and Clocks

_Peios / Advanced Peios / peinit / Timers_

> The random delay applied to each firing, which clock a timer is evaluated against, and what a clock change does.

## 9.4.1 Jitter

`TimerJitter`, zero by default, adds a random delay to each firing.
peinit draws a uniformly random whole number of seconds from zero to
`TimerJitter` inclusive, from the kernel's random source, and adds it to
the computed occurrence.

The delay is recomputed on every firing, so a daily timer with
`TimerJitter=900` fires at a different moment between 00:00 and 00:15
each day. With `TimerJitter=0` no randomness is consulted at all.

Jitter is applied **after** the calendar expression is evaluated and is
only ever added, so a timer never fires early — only late.

The boot catch-up firing is not jittered. It fires immediately, and
jitter applies from the next armed occurrence onward.

## 9.4.2 Which clock

The split is the point of this section, and it is not cosmetic.

**Calendar timers are wall-clock schedules**, so they are armed as
absolute `CLOCK_REALTIME` timers: `timerfd_settime` with
`TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET`. `CANCEL_ON_SET` makes the
descriptor's read return `ECANCELED` whenever the realtime clock is
discontinuously changed — an NTP step, a manual set. peinit recomputes
the next occurrence against the new wall clock and re-arms, which is
what keeps `*-*-* 02:00:00` anchored to 02:00 across clock corrections.

**Interval timers are genuine relative durations** and use
`CLOCK_MONOTONIC`: the watchdog, health check intervals and timeouts,
restart backoff, and the Start, Stop and Reload phase timeouts. "Wait
thirty seconds" means thirty elapsed seconds regardless of what happens
to the wall clock. They are not armed with `CANCEL_ON_SET`, correctly —
a monotonic timer has no reason to be cancelled by a realtime set.

These are not separate descriptors. Every interval deadline is
aggregated onto one monotonic timerfd armed to the earliest of them.

**Last-run timestamps are recorded on `CLOCK_REALTIME`**, since they
record when a timer actually fired in wall-clock terms. The same firing
passes a monotonic timestamp into the operation machinery, because
operation timing is elapsed time.

## 9.4.3 Clock events

- **A realtime step at runtime.** The armed timer is cancelled; peinit
  recomputes against the new wall clock and re-arms. A backward step
  pushes the next firing later; a forward step that crosses an
  occurrence fires it once. If the step lands inside a jitter window,
  the firing happens at the un-jittered scheduled time — later than the
  schedule, never earlier.
- **Suspend and resume.** An absolute deadline that elapsed while
  suspended fires once on resume. The expiration count is ignored, so a
  long suspend produces one firing, not one per occurrence.
- **A missed occurrence within one uptime.** Fire once, then compute the
  next future occurrence. peinit never replays every occurrence that
  elapsed during a gap — the same rule as the cross-reboot catch-up.
- **A backward jump across a boot.** If the last-run timestamp is in the
  future relative to the current wall clock at boot, peinit treats the
  history as unknown and fires the catch-up immediately. This check is
  boot-time only; there is no runtime equivalent.
- **A wrong clock at boot.** A system that boots with a badly wrong
  clock and has NTP correct it later may fire a persistent catch-up
  spuriously or not at all. The runtime half is covered by
  `CANCEL_ON_SET` — once NTP corrects the clock, armed calendar timers
  are cancelled and recomputed — but the boot-time catch-up decision has
  already been made by then. Short of NTP-aware rescheduling, this
  remains an edge.

> [!NOTE]
> The calendar parser deserves heavy testing. Time parsing is a rich
> source of edge cases: month boundaries, leap years,
> last-day-of-month arithmetic, DST transitions and timezone database
> updates are all fertile ground for subtle errors, and every one of
> them is a bug that only appears on a particular day of a particular
> year.

---

# 10.1 The Control Socket

_Peios / Advanced Peios / peinit / The Control Interface_

> The Unix stream socket every runtime command arrives on — how it is created and protected, and the peer token it captures.

peinit serves every runtime command on a Unix stream socket at
`/run/services/peinit/control.sock`, created during Phase 1
infrastructure setup and existing for the lifetime of the system. Its
wire protocol is specified in PSPU §4; this chapter is how peinit
implements its side.

## 10.1.1 Creation and protection

The socket is created with `SOCK_CLOEXEC | SOCK_NONBLOCK` and a listen
backlog of 32, and unlinked when peinit drops it. Accepted connections
come from `accept4` with both flags, so no connection descriptor is ever
inherited by a service.

peinit sets **no POSIX mode bits** on the socket, on the notification
socket, or on anything else it creates. Under KACS, mode bits are not
what governs access — a Security Descriptor is — so setting them would
be inert.

What governs access is inheritance. `/run` is a tmpfs peinit mounts
itself in Phase 1, and a fresh tmpfs carries no descriptor at all, which
under `DENY_MISSING` would leave every inode on it unreachable to
everything. So peinit stamps the mount root with an inheritable
descriptor as soon as it mounts it (§2.3):

```
O:SY G:SY D:(A;OICI;GA;;;SY)
```

Every inode created underneath inherits from it, the two sockets
included. The parent directory `/run/services/peinit/` is created
plainly, with no descriptor of its own, so it inherits too.

The effect is that both sockets are reachable by SYSTEM and by nothing
else. The single inheritable entry grants `GENERIC_ALL` to `S-1-5-18`
and names no other principal, and connecting to a pathname socket is
checked against the socket inode's descriptor before any peer identity
is established.

## 10.1.2 Connections

peinit accepts a connection, obtains the peer's token, and only then
admits it against the connection limit:

| Key | Default | Meaning |
|---|---|---|
| `Machine\System\Init\MaxControlConnections` | 32 | Concurrent connections. |
| `Machine\System\Init\MaxRequestSize` | 65536 | Maximum request size, in bytes. |
| `Machine\System\Init\ConnectionTimeout` | 30 | Seconds before an idle connection is closed. |

A connection over the limit is closed at the socket level, before any
request is read and without a response — there is no error code for it,
because there is no protocol state in which to deliver one. A peer whose
token cannot be obtained is closed the same way.

## 10.1.3 The peer token

The token is captured **once**, when the connection is accepted, using
`kacs_open_peer_token`. It is the peer thread's *effective* token at
that moment, so a peer that was impersonating is captured as the
impersonated identity — which is what makes access decisions reflect the
identity a client is actually operating under rather than its underlying
service identity.

Because it is captured once, a peer that changes identity mid-connection
is still evaluated against the identity it connected with.

## 10.1.4 Idle and waiting

A connection is idle only when it has nothing in flight. One blocked on
a `wait=true` operation, or with output still buffered, is never idle
and is never closed by `ConnectionTimeout` — it stays open until the
operation resolves, bounded by the operation's own timeout rather than
the connection's.

peinit handles one frame per readiness turn, and reads no further frames
from a connection while a wait is pending on it. Pipelined requests are
therefore serialised behind a wait.

## 10.1.5 Timestamps

Every timestamp peinit puts on the wire is derived by projecting a
monotonic event stamp through the current offset between the realtime
and monotonic clocks. Elapsed-time decisions stay monotonic; only the
presentation is wall-clock.

---

# 10.2 Dispatch and Authorisation

_Peios / Advanced Peios / peinit / The Control Interface_

> The fixed sequence a parsed command runs before it does anything — the shutdown gate, the rights checked, and what is filtered rather than denied.

A parsed command runs a fixed sequence before it does anything.

1. **The shutdown gate.** If peinit is shutting down, everything except
   `status`, `list` and `operation-status` is rejected. The gate runs
   before the access check, so during shutdown a caller who would have
   been denied is told the command is invalid for the current state
   rather than that they lack the right.
2. **Resolve the target.** A command naming no definition, and no
   addressable definition-removed entry, returns `UNKNOWN_SERVICE`.
   peinit does not synthesise a descriptor for something that does not
   exist.
3. **AccessCheck.** The caller's token, the target's descriptor, the
   generic mapping, and the right the command requires (§4.6, §4.7).
4. **On denial**, return `ACCESS_DENIED` and record an `access.denied`
   event carrying the caller's SID, the target, the requested right by
   name, and the access bits requested and granted. Silent denial is not
   acceptable; a denial an administrator cannot see is
   indistinguishable from a bug.
5. **On grant**, classify the command against the service's state
   (§10.3) and act.

## 10.2.1 Rights

| Command | Right |
|---|---|
| `start` | `SERVICE_START` |
| `stop` | `SERVICE_STOP` |
| `restart` | `SERVICE_START` and `SERVICE_STOP` |
| `reload` | `SERVICE_INTERROGATE` |
| `reset` | `SERVICE_STOP` |
| `status` | `SERVICE_QUERY_STATUS` |
| `list` | Filtered per service by `SERVICE_QUERY_STATUS` |
| `operation-status` | `SERVICE_QUERY_STATUS` on the target service |
| `shutdown` | `SYSTEM_SHUTDOWN` |
| `reload-config` | `SYSTEM_RELOAD_CONFIG` |

`operation-status` resolves the operation before it checks the right, so
an unknown identifier is reported as unknown regardless of who asked.

## 10.2.2 Filtering

`list` checks every service and partitions the result. Services the
caller can query are returned; services it cannot are omitted, and the
denials become audit events rather than anything the caller sees. A
caller with no query rights anywhere gets an empty list and a successful
response, not a denial — the filtering exists to avoid answering the
question "does this service exist", and reporting the denials would
answer it.

Definition-removed services are listed, and the list entry does not say
so. A `status` query on one does.

---

# 10.3 The Command × State Matrix

_Peios / Advanced Peios / peinit / The Control Interface_

> The defined answer for every command sent to a service in an unexpected state, including backoff, abandoned and definition-removed services.

A command sent to a service in an unexpected state gets an answer, not a
silent no-op. What the answer is depends on the pair.

| | Inactive | Starting | Active | Reloading | Stopping | Completed | Backoff | Failed | Abandoned | Skipped |
|---|---|---|---|---|---|---|---|---|---|---|
| start | Start | MERGE | ALREADY | ALREADY | QUEUE | Start | DEFER | Start | ERROR | Start |
| stop | NOOP | Cancel+Stop | Stop | Stop | MERGE | Clear | Cancel | NOOP | ERROR | NOOP |
| restart | Start | QUEUE | Restart | Restart | QUEUE | Start | Restart | Start | ERROR | Start |
| reload | ERROR | ERROR | Reload | MERGE | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR |
| reset | NOOP | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR | Clear | Clear | Clear |
| status | OK | OK | OK | OK | OK | OK | OK | OK | OK | OK |

**ALREADY** — the service is already where the command would take it and
no operation of that type is in flight. peinit returns the current
status rather than an error.

**MERGE** — an operation of that type is already running. The command
merges into it; the caller receives that operation's identifier and, if
waiting, blocks on its outcome.

**DEFER** — create a Pending start operation but do not execute it until
the existing backoff deadline expires. A deferred start already present
is merged into.

**QUEUE** — the operation is queued Pending and executes after the
current one completes.

**NOOP** — the command has no effect. peinit returns the status.

**ERROR** — the command is invalid for the state.

**Clear** — reset to Inactive.

**Cancel** — abort the current operation, then proceed.

## 10.3.1 The Backoff column

Backoff is the interesting one, because the service is down with an
automatic restart already pending.

- `start` creates or merges into a **deferred** start operation and
  honours the remaining delay. It does not short-circuit the backoff.
  If the automatic restart later becomes due, it merges into the
  administrator's operation, so the identifier the caller holds is the
  one that executes.
- `stop` cancels both the pending restart and any deferred start, and
  the service goes Inactive. A subsequent automatic restart is refused,
  because the service is no longer in Backoff.
- `restart` cancels the automatic restart and queues an
  administrator-initiated one.
- `reload` and `reset` are invalid: there is no process to reload, and
  no terminal state to clear.

## 10.3.2 The Skipped column

`start` and `restart` clear Skipped before they run. A Skipped service
is not in a state a start can proceed from — the state machine permits
`Skipped -> Inactive` and nothing else — so the activation performs that
transition first, then re-evaluates the conditions from scratch. Both
outcomes are possible: the precondition that was missing at boot may now
hold, in which case the service starts; or it may still not, in which
case the service is skipped again, for whatever reason applies now.

The clear is reported like any other transition, so a console watching
the service sees it leave Skipped rather than appearing to jump.

`reset` also clears Skipped, and differs only in stopping there.

## 10.3.3 The Abandoned column

Every lifecycle command is invalid on an Abandoned service except
`reset`, which clears it (§6.2). Nothing else is meaningful while
processes that ignored SIGKILL are still in the cgroup.

## 10.3.4 Definition-removed services

Independently of state, a service whose definition has been removed
(§3.8) rejects `start`, `restart` and `reload` with `UNKNOWN_SERVICE`,
accepts `stop`, and reports its state on `status`.

---

# 10.4 reload-config

_Peios / Advanced Peios / peinit / The Control Interface_

> A full atomic re-read of the registry rather than a live update — what changes, and how removals and the compiled-in service are handled.

`reload-config` takes a fresh snapshot of the configuration from the
registry. It does not live-update anything.

It is also the path a registry change notification takes: any drained
watch event triggers the same full reload, rather than a targeted
re-read of whatever changed.

## 10.4.1 Atomicity

peinit reads everything first. Every registry read happens before any
mutation, so a read failure returns an error with nothing touched. It
then builds and validates a complete new graph in memory, and only swaps
it in if validation succeeds.

If validation fails, the previous generation stays live and the findings
are returned to the caller. This is where the reload path differs
sharply from boot: boot marks individual services Failed and carries on,
because it has to produce a running system; reload rejects the whole
thing, because it has a running system already and a half-applied
configuration would be worse than the one in place.

## 10.4.2 What changes

- Every service definition is re-read.
- A new dependency graph is built and validated.
- Running services are unaffected and continue on their activation
  generation.
- New definitions take effect at the next start, restart, or trigger.
- New services become available for `start` immediately.
- Timer triggers are re-evaluated and every calendar timer is re-armed,
  from the current time and with no catch-up.

A reload also refreshes things that are not service definitions: the
control descriptor, the three control socket limits, the log
configuration, the shutdown settings, the global environment layer, and
the eventd log socket path. It also prunes the fd stores of services
that no longer exist.

## 10.4.3 Removals and the compiled-in service

A definition that has disappeared is handled by §3.8 — discarded if
nothing is running, marked definition-removed if something is.

registryd is exempt. Its compiled-in definition survives a reload that
does not mention it, and its provenance survives a registry entry that
shadows it. peinit started registryd before the registry existed, and a
reload finding no definition for it cannot conclude that it should stop
being managed.

---

# 10.5 The Notification Socket

_Peios / Advanced Peios / peinit / The Control Interface_

> The datagram socket services report on themselves over — authenticating a sender, applying a datagram, and the bounds on it.

peinit binds one Unix datagram socket for service notifications, by
default at `/run/services/peinit/notify.sock`. Its path is an
implementation detail: services receive it through `NOTIFY_SOCKET` and
nothing hardcodes it. The kernel command line can override it with
`peios.notifysocket=`.

There is one socket, not one per service, and the bind unlinks any stale
path first. It carries the same inherited descriptor as the control
socket (§10.1).

The protocol itself is PSPU §4. What follows is how peinit decides
whether to believe a datagram.

## 10.5.1 Authenticating a sender

`SO_PASSCRED` is enabled on the socket, so every datagram arrives with a
kernel-attested `SCM_CREDENTIALS` control message. A datagram without one
is rejected outright. Descriptors for the fd store arrive alongside, as
`SCM_RIGHTS`.

Authentication then runs five steps, and each closes a hole the previous
one leaves:

1. **Find the sender.** Scan for the current *service-main* job whose
   PID equals the sender's. Only a main job is ever a candidate, which
   is what makes `NotifyAccess=Main` the only mode there is — a hook or
   a health check cannot notify on a service's behalf.
2. **The job is Running.** A job still in pending setup has not exec'd.
3. **The job has a pidfd.**
4. **The pidfd still refers to that PID.** This is the step that
   matters: PID matching alone is racy, because a PID can be recycled
   between the sender writing and peinit reading. The pidfd was obtained
   atomically at fork, so verifying the PID against it is what makes the
   match sound rather than probable.
5. **The generation matches.** A job whose activation generation is
   not the service's current one is a previous incarnation, and its
   notifications are rejected. This is invariant 5 of §6.1 in force: a
   `READY=1` from the process that just crashed cannot mark its
   replacement ready.

Anything that fails is dropped and recorded as a `notify.rejected` event
carrying the sender's PID and the reason.

The UID and GID in the credentials are parsed and never used. They are
not policy inputs, and peinit does not consult them for anything —
identity on Peios is a token, and the token here is established by
which job the sender *is*, not by what UID it claims.

## 10.5.2 Applying a datagram

A datagram may carry several newline-separated lines, and peinit applies
every one. Parsing happens before application and is all-or-nothing: if
any line is malformed, nothing from that datagram is applied, and any
descriptors it carried are dropped and closed. Partial application of an
ambiguous service-control message is structurally impossible rather than
merely avoided.

A rejection is recorded after authentication, so the event can name the
service where one could be established.

## 10.5.3 What the fields do

Most are handled elsewhere: `READY=1` and `RELOADING=1` in §6.5,
`WATCHDOG=1` and `WATCHDOG_USEC` and `EXTEND_TIMEOUT_USEC` in §6.6,
`STOPPING=1` in §12.2, and the fd store fields in §10.6.

Three are event-emitting. `STATUS=`, `ERRNO=` and `EXIT_STATUS=` are
authenticated and then emitted as KMES events — `notify.status`,
`notify.errno`, `notify.exit_status` — whose payloads carry the service
name, the job identifier, the operation identifier and the activation
generation, alongside the value. They take the same path as job and
operation events, not a forward to eventd.

`STATUS=` is additionally stored on the service's runtime state and
exposed as `status_text` in a status query. It is cleared to null at the
start of every activation generation, in the same step that increments
the generation, so a status string cannot survive a restart and describe
a process that no longer exists.

`ERRNO=` and `EXIT_STATUS=` are not stored. They are emitted and
otherwise not retained.

## 10.5.4 Bounds

A datagram is read into a fixed 64 KiB buffer, and the control message
buffer is sized for 64 descriptors. Neither `MSG_TRUNC` nor `MSG_CTRUNC`
is inspected, so a larger datagram is truncated silently and descriptors
beyond the sixty-fourth are dropped by the kernel before peinit sees
them.

---

# 10.6 The Fd Store

_Peios / Advanced Peios / peinit / The Control Interface_

> Keeping file descriptors across a service's own restart — storing, removing, injecting and clearing them.

The fd store lets a service keep file descriptors across its own
restart. It pushes them to peinit, peinit holds them, and the new
process gets them back. That is what lets a stateful daemon — a web
server holding a listening socket, say — restart without dropping
connections it has already accepted.

`FdStoreMax` in the definition sets the maximum number of descriptors
peinit will hold. It defaults to **0**, which disables the store: most
services do not need it, and holding descriptors on behalf of a service
that will never ask for them back is pure cost.

## 10.6.1 Storing

When an authenticated datagram carries `FDSTORE=1` with descriptors
attached:

1. If `FdStoreMax` is 0, peinit logs the rejection and **closes** the
   descriptor.
2. If the store already holds `FdStoreMax` entries, peinit logs the
   rejection and closes the descriptor. The existing store is not
   modified — a full store does not evict.
3. `FDNAME=<name>` names it; an absent or empty name means `stored`.
4. `FDPOLL=0` marks the descriptor exempt from poll monitoring.

Either rejection emits an `fd_store.rejected` event carrying the outcome
and the reason, so a service whose descriptors are being silently
dropped can find out why.

Several descriptors may share a name. One `FDSTORE=1` carrying N
descriptors creates N entries under the one name, each independently
subject to the limit — so the first few fit and the overflow is
rejected and closed.

peinit does not monitor stored descriptors. The `FDPOLL` flag is
recorded and nothing reads it, so no stored descriptor is evicted for
becoming invalid.

## 10.6.2 Removing

`FDSTOREREMOVE=1` with `FDNAME=<name>` removes every descriptor of that
name and closes them. A name matching nothing is a no-op rather than an
error.

`FDSTOREREMOVE=1` **without** a name aborts the whole fd-store step for
that datagram — so a datagram carrying both an unnamed remove and an
`FDSTORE=1` performs neither, and the attached descriptors are dropped
and closed.

## 10.6.3 Injecting

When a service restarts, peinit injects the stored descriptors during
the child's pre-exec path (§5.4):

1. They are placed consecutively from `SD_LISTEN_FDS_START` — descriptor
   3 — upward, with close-on-exec cleared: the only sanctioned
   exception to the close-on-exec discipline.
2. `LISTEN_FDS` is set to the count.
3. `LISTEN_FDNAMES` is set to a **colon-separated** list of names in the
   same order as the descriptor numbers.
4. The store is cleared. peinit no longer holds them.

Both variables are omitted entirely when the store is empty.
`LISTEN_PID`, which a conforming client checks against its own PID
before trusting `LISTEN_FDS`, is not set.

Injection happens for the main process only. Hooks and health checks
never receive stored descriptors.

The store is cleared on a successful injection, at the top of the
started-launch handling. A launch that *fails* does not clear it, so the
descriptors survive a failed attempt and are available to the next one.

## 10.6.4 Clearing

The store is cleared, and its descriptors closed, when:

- **The service is stopped explicitly** — by an administrator, or by
  shutdown. The distinction peinit draws is the operation's type and
  source together: an administrator's stop clears, and a
  restart-policy-sourced stop does not. The service is not coming back
  from an explicit stop, so the descriptors are no longer useful.
- **The definition is removed** and its entry finally discarded (§3.8),
  immediately if nothing was running and on the instance's exit
  otherwise.

It **survives** an automatic restart — crash, restart policy, new start
— which is the entire point. The descriptors persist through exactly the
restart the service did not choose and cannot prepare for.

> [!NOTE]
> The `LISTEN_FDS` and `LISTEN_FDNAMES` convention is systemd's, so
> software already written to receive descriptors that way works
> unmodified. peinit's own C client library exposes the notification
> helpers but no descriptor-passing helper, so reaching the store from
> that library means constructing the control message by hand.

---

# 11.1 Wiring

_Peios / Advanced Peios / peinit / Output Handling_

> peinit is not a logging system but it holds the pipes at birth — where a service's output goes, how it is tagged, and terminal-attached services.

peinit is not a logging system — eventd stores, indexes and queries
logs. But peinit holds the pipes at birth. It decides where a service's
output goes, and it has to cover the window before eventd exists.

## 11.1.1 The pipes

Before exec, peinit creates a pipe for stdout and one for stderr with
`pipe2(O_CLOEXEC)`. The child's streams are redirected onto the write
ends; peinit keeps the read ends and watches them with epoll.

The blocking discipline is asymmetric, deliberately:

- The parent's **read** ends are non-blocking. PID 1 cannot afford to
  block on a read.
- The child's **write** ends keep ordinary blocking semantics.

That second half is what makes backpressure work. When a service
produces faster than peinit consumes, the kernel pipe buffer fills and
the service's own `write()` blocks, so the service slows down. Making
the write ends non-blocking would turn that into `EAGAIN` failures in
the service, converting a flow-control mechanism into an error the
service has to handle.

The child's stdin is redirected to `/dev/null`. peinit provides no
interactive input channel; a service needing input obtains it explicitly
— through a socket, or a stored descriptor — never through inherited
stdin.

The epoll instance itself is close-on-exec and is never inherited.

## 11.1.2 Tagging

peinit reads output line by line and tags each line with:

- the origin,
- the stream — stdout or stderr,
- a `CLOCK_REALTIME` timestamp,
- the job's identifier.

The origin is the service name for a main process. For a hook it is the
service name, the hook kind and its index — `jellyfin/ExecStartPre[0]`.
Reload commands are `<service>/ExecReload` and health checks
`<service>/HealthCheck`.

Health check output is captured, which is usually the only way to find
out why a health check failed.

## 11.1.3 Terminal-attached services

A service with a `TTYPath` has all three streams on its terminal, and
both pipe pairs are closed in the child (§5.4). Its output is not
captured at all — it goes to the terminal, which is what asking for one
means.

---

# 11.2 The Pre-Eventd Buffer

_Peios / Advanced Peios / peinit / Output Handling_

> There is nowhere to send output until eventd binds its socket — what peinit buffers in memory, and why audit records are not logs.

Before eventd starts there is nowhere to send service output: eventd's
log socket does not exist until eventd binds it. peinit buffers in
memory until then, in a bounded buffer that drops the **oldest**
entries when it fills.

| Key | Default | Minimum | Meaning |
|---|---|---|---|
| `Machine\System\Init\PreEventdBuffer` | 1048576 | 4096 | Bytes of output retained before eventd exists. |

A value below the minimum keeps the default and logs a warning, rather
than being honoured or failing the boot (§11.3). Zero is the case that
matters: a zero-capacity buffer rejects every record, so honouring it
would silently discard the whole pre-eventd window.

The value is read from the registry at boot and refreshed on reload, and
the buffer adopts the new capacity each time. Lowering it takes effect
immediately, dropping the oldest entries until the contents fit — the
same end of the buffer a steady-state overrun drops.

Dropping the oldest rather than the newest is the right way round for
this buffer: the point of the window is the boot that is happening now,
and the most recent output is what explains where it got to.

> [!NOTE]
> In practice the only services that run before eventd are registryd,
> lpsd, authd and eudev. The first three are Peios-owned with controlled
> output. eudev is the real overrun risk, being verbose about device
> enumeration. A crash loop's output is bounded by the restart budget.

## 11.2.1 Audit records are not logs

peinit's own audit records — access denials, critical failures, recovery
mode entry, graph errors, security-relevant transitions — are **events**
rather than logs. They go into the KMES kernel ring buffer, which
persists from the moment PKM loads: before eventd, before the registry,
before Phase 2.

So there is no pre-eventd buffer for them, and no window in which they
could be lost. eventd picks them up from the ring buffer when it
attaches, wherever in the boot they were emitted.

That distinction is the reason the two paths exist at all. Logs are
best-effort and lossy by design; audit events are neither.

---

# 11.3 Flood Protection

_Peios / Advanced Peios / peinit / Output Handling_

> The three bounds that stop a noisy service starving the event loop, and where loss is allowed and where it is not.

A noisy service cannot be allowed to starve the event loop. Three bounds
apply.

| Key | Default | Minimum | Meaning |
|---|---|---|---|
| `Machine\System\Init\MaxLogLineLength` | 8192 | 256 | Bytes per line before truncation. |
| `Machine\System\Init\MaxLogBufferPerService` | 65536 | 4096 | Bytes buffered per service pipe before backpressure. |
| `Machine\System\Init\LogReadBytesPerEvent` | 16384 | 512 | Bytes drained from one pipe per readable event. |

A value below the minimum is **not** honoured and does **not** fail the
boot: the compiled-in default is used and a warning is logged naming the
key, the configured value and what was used instead. The same rule
covers `PreEventdBuffer` (§11.2), and it is the rule peinit already
applies to the equivalent kernel command-line knobs — a typo in a
logging knob must not decide how the machine boots, and must not be
silent either.

`MaxLogBufferPerService`'s minimum is the one with an external cause:
`F_SETPIPE_SZ` will not go below one page and rounds up regardless, so a
smaller number in the registry would describe a pipe that does not
exist. The other minimums are engineering judgement — the point below
which the mechanism stops working rather than working differently.

A line exceeding `MaxLogLineLength` is truncated and marked
`[truncated]`, with the content trimmed so that content and marker
together come to exactly the limit. Everything up to the next newline is
then suppressed rather than emitted as a second line.

`MaxLogBufferPerService` is applied as the pipe's own capacity through
`F_SETPIPE_SZ`, which is a literal reading of "bytes buffered per
service pipe before backpressure" — the kernel does the buffering and
the bound is where it belongs.

`LogReadBytesPerEvent` bounds one readable event rather than one loop
iteration. A turn with several ready pipes reads up to the budget from
each, bounded overall by how many events one epoll wait returns.

## 11.3.1 Where loss is allowed and where it is not

At the pipe stage, peinit does not drop. It keeps reading within its
budget and appends every complete line; only `EAGAIN`, end of file, or
an error stops the read. Backpressure through the pipe is the
flow-control mechanism between a service and peinit, and dropping there
would replace it with silence.

Downstream of the pipe, delivery is loss-tolerant by design. The
pre-eventd buffer drops its oldest entries when full, and eventd's
datagram socket may drop records under load. The no-silent-drop
guarantee covers *reading the pipe*, not delivery to eventd.

Audit events are exempt from all of it. They go through KMES.

## 11.3.2 Event loop fairness

No source may starve the loop. Signals are handled at the highest
priority — SIGCHLD reaping and shutdown handling take precedence over
everything else in every iteration — with the shutdown deadline timer
next and every other source below that, ties broken by arrival order.

The power button shares the top priority with signals, on the reasoning
that someone physically pressing it is asking for the same class of
attention.

---

# 11.4 The eventd Handoff

_Peios / Advanced Peios / peinit / Output Handling_

> Switching from buffering to forwarding when eventd reaches Active — the record format, lossy delivery, and what happens when eventd goes away.

When eventd reaches Active, peinit switches from buffering to
forwarding.

1. Start sending to eventd's log datagram socket, at the path from
   `Machine\System\eventd\LogSocketPath`.
2. Replay the pre-eventd buffer, oldest first, preserving each line's
   timestamp and metadata. The replay is best-effort: these are
   datagrams on a loss-tolerant socket, so some may be dropped, and
   peinit does not block waiting to deliver them.
3. Switch to real-time forwarding — new output sent as it arrives.
4. Clear the buffer.

From there peinit is a relay: read from the pipes, tag each line,
forward. Audit events continue to flow as KMES events, entirely
separately.

## 11.4.1 The record

Each record is a msgpack map:

| Key | Type | Content |
|---|---|---|
| `origin` | string | The service name, or the hook identifier. |
| `is_error` | bool | True for stderr. |
| `message` | string | The line. |
| `timestamp` | uint | Nanoseconds, wall clock. |
| `job_id` | bin | The job's 16-byte GUID. Omitted when absent. |

The map has four or five entries depending on whether a job identifier
applies.

## 11.4.2 Lossy delivery

eventd's log socket is a non-blocking Unix datagram socket. If eventd
cannot drain it fast enough its `SO_RCVBUF` fills and the kernel drops
further datagrams silently — log ingestion deliberately exerts no
backpressure on senders.

peinit therefore keeps no outbound write buffer. It sends each record as
a datagram and accepts that some may be dropped. It never blocks on a
send, and never lets pending records grow without bound.

Each send opens a datagram socket, sends, and closes it. Records go one
at a time; there is no batching of several records into one datagram.

A send that fails — the receive buffer full — takes peinit out of
forwarding for the remainder of that turn: the failing record and the
rest of its batch go back into the pre-eventd buffer. The end of the
turn re-establishes forwarding by replaying them.

## 11.4.3 When eventd goes away

peinit supervises eventd like any other service, so it sees the exit
directly. It re-enables the pre-eventd buffer, and when eventd restarts
and reaches Active the handoff repeats.

There is a log gap between eventd crashing and restarting, bounded by
the buffer size. Events are unaffected: they land in the KMES ring
buffer regardless of eventd's state, and eventd resumes consuming from
the last persisted sequence when it comes back.

---

# 11.5 Console Output

_Peios / Advanced Peios / peinit / Output Handling_

> peinit writes its own operational messages to the console and never a service's output — plus severity and the quiet setting.

peinit writes its own operational messages to `/dev/console`:

- Phase 1 progress — mount results, registryd starting.
- Phase 2 progress — services starting and failing, dependency errors.
- Shutdown progress.
- Recovery mode entry.
- Critical service failures.

Service output is never echoed to the console. The console is for
peinit's own messages; a service that wants a terminal asks for one with
`TTYPath`.

## 11.5.1 Severity and quiet

Each message carries a severity, and `peios.quiet` (§2.6) decides what
that means:

- At `0`, everything is written.
- At `1`, the default, peinit stays out of a terminal held as the
  controlling terminal of a running service, except to announce loss of
  the system. Terminals are matched by device rather than by path, since
  `/dev/console` and `/dev/ttyS<n>` can be the same device; where the
  device cannot be determined peinit assumes the terminal is held.
- At `2`, ordinary progress is dropped everywhere while errors still
  get through.

Suppressed messages are discarded rather than buffered for later.

Shutdown progress carries ordinary status severity, so `peios.quiet=2`
suppresses it along with every other kind of progress.

The autorun step in Phase 1 (§2.3) bypasses the policy entirely, on the
grounds that a script running that early and going wrong is worth
interrupting anything for.

---

# 12.1 Triggers

_Peios / Advanced Peios / peinit / Shutdown_

> The four paths that initiate a shutdown — the control socket, signals, the power button and a Critical service failure.

Four paths initiate a shutdown.

## 12.1.1 The control socket

A `shutdown` command naming a type, gated on `SYSTEM_SHUTDOWN` against
peinit's control descriptor (§4.7):

| Type | Effect |
|---|---|
| `poweroff` | Stop everything, unmount, power off. |
| `reboot` | Stop everything, unmount, reboot. |
| `halt` | Stop everything, unmount, halt — the CPU stops, the system stays powered. |

## 12.1.2 Signals

| Signal | Meaning |
|---|---|
| SIGINT | Reboot. The kernel sends it on Ctrl+Alt+Del. |
| SIGTERM | Poweroff. PID 1 cannot be killed by it but may choose to act on it. |
| SIGPWR | Poweroff. The compatibility path for environments that surface power failure or a power-button policy as a signal. |

**Three SIGINTs within five seconds force an immediate shutdown**: no
graceful stop, no ordering, SIGKILL every service cgroup, sync, reboot.
The window is a sliding five seconds and the press is recorded before
the already-shutting-down check, so three presses still force even after
a graceful reboot has begun. That is the point — someone pressing it
three times has decided the graceful path is not working.

## 12.1.3 The power button

An `EV_KEY` / `KEY_POWER` press from a readable `/dev/input/event*`
device is a graceful `poweroff`. Only a press — value 1 — initiates.
Releases, key repeats, other keys and other event types are ignored.

The path is fail-soft throughout: a missing `/dev/input`, a device that
cannot be opened or registered, and a registered descriptor that later
fails to read are all survivable, and a failing descriptor is removed
from the event loop so repeated failures cannot spin PID 1. Losing it
degrades only direct power-button handling; the socket and signal paths
remain.

It is deliberately minimal. It is not a power-management policy engine
and does not replace a future daemon that would translate richer policy
into control socket commands.

## 12.1.4 Critical service failure

A Critical service entering Failed with its restart budget exhausted
means peinit syncs the filesystems and reboots immediately. This is not
a graceful shutdown: there is no stop ordering, no seed save, and no
unmount. The system is in an undefined state and the fastest path to a
defined one is a reboot.

---

# 12.2 The Graceful Sequence

_Peios / Advanced Peios / peinit / Shutdown_

> The ordered steps of a graceful shutdown, from entering the shutdown state to the final power action.

## 12.2.1 Step 1: Enter the shutdown state

peinit sets an internal flag. While it is set, no new service starts,
and control commands other than `status`, `list` and `operation-status`
are rejected as invalid for the current state.

Timer triggers are not disarmed. A calendar timer that fires during the
shutdown window is still classified and acted on: for a service in
Inactive, Completed or Failed — precisely the states step 3 classifies
as not participating — that means creating a start operation and
starting the service. The shutdown plan was fixed when the shutdown
began, so a service started this way is in no wave, is not waited for,
and is not reached by the global-timeout sweep.

## 12.2.2 Step 2: Suspend Critical failure semantics

A Critical service failing during shutdown is recorded but does not
trigger a reboot. The system is already going down, and rebooting from
here would loop.

## 12.2.3 Step 3: Classify

Completed services — Oneshots with `RemainAfterExit` — have no process
and are transitioned to Inactive, releasing the dependency
relationships they were holding so their dependents can be stopped
cleanly.

The rest are classified for stop eligibility:

- **Active and Reloading** are graceful-stop eligible and enter the
  waves.
- **Stopping** services are already on a stop path. They join the waves
  for ordering and timeout purposes, but do **not** receive another
  SIGTERM.
- **Starting** services are not eligible. peinit cancels the startup,
  SIGKILLs the service cgroup if one exists, and transitions them to
  Failed with cause `ShutdownWave` while post-kill verification is
  pending. A cgroup still populated after the post-kill timeout takes
  the service to Abandoned with cause `ProcessUnkillable` and the cgroup
  is leaked. A Starting service whose job never forked skips the check
  and goes straight to Failed.
- **Inactive, Failed, Skipped, Backoff and Abandoned** do not
  participate. Abandoned cgroups stay leaked and shutdown continues.

## 12.2.4 Step 4: Stop in reverse dependency order

peinit builds the reverse dependency graph over hard dependencies and
stops in waves:

1. Each eligible service receives SIGTERM. Already-stopping ones do not.
2. Each has `StopTimeout` to exit.
3. On expiry, SIGKILL to the service's entire cgroup.
4. No service is stopped until everything depending on it has stopped.

A service that sent `STOPPING=1` does not receive a SIGTERM at all: it
has already said it is shutting down, and peinit goes straight to the
stop deadline.

### 12.2.4.1 Timing an already-stopping service

peinit does not reset an already-Stopping service's clock, either when
shutdown begins or when its wave becomes eligible. It uses the timing
evidence retained from the stop path that put the service in Stopping:

- If a Stop operation, or a Restart executing its stop leg, is in
  flight, that operation's retained timing governs.
- Otherwise peinit uses service-level evidence, which carries the cause
  that initiated the transition — `ExplicitStop`, `ShutdownWave`,
  `ConflictEviction` or `BindsToPropagation`.

peinit requires the evidence to be present, to belong to a service
actually in Stopping, to carry a cause matching the service's current
cause, to name one of those four causes, and not to describe a deadline
earlier than its own start. Evidence that is missing, stale or
ambiguous **fails closed** rather than being guessed at — and it fails
the whole shutdown, not just that participant, so a shutdown command
with one such service is refused, and one discovered on a later wave
ends the runtime loop.

An operation whose lifetime expires while it is still Pending — a
later-wave stop waiting for its dependencies — fails that operation and
its waiters. It does **not** authorise signalling the service before its
wave is eligible. Shutdown owns the signalling; the operation object is
an observation of it.

## 12.2.5 Step 5: Global timeout

| Key | Default | Meaning |
|---|---|---|
| `Machine\System\Boot\ShutdownTimeout` | 90 | Seconds for the whole sequence. |
| `Machine\System\Boot\PostKillTimeout` | 5 | Seconds for a cgroup to drain after SIGKILL. |

On expiry, every remaining participant's cgroup is killed, anything that
does not drain within the post-kill timeout is marked Abandoned with its
cgroup leaked, and shutdown continues regardless.

## 12.2.6 Step 6: Save the random seed

After every service has stopped and before any unmount, peinit writes a
fresh seed to `/var/state/peinit/random-seed` for the next boot: 512
bytes from the kernel CSPRNG on this machine, never copied from an
image, protected so that only SYSTEM-equivalent authority can read or
replace it.

The write is crash-conscious: a temporary file on the same filesystem,
written, flushed, atomically renamed over the old seed, and the
containing directory flushed. A failure is recorded and shutdown
continues.

The forced and Critical-reboot paths skip it, along with the unmount
step, and go directly to sync and the final action.

## 12.2.7 Step 7: Unmount

1. Snapshot the mount table first, from `/proc/self/mountinfo`.
2. Attempt to unmount every remaining non-root mount in the namespace —
   not only what peinit mounted, so the Phase 1 set is covered by
   construction.
3. Process in descending path depth, so children go before parents.
4. A mount point already gone is a successful no-op.
5. On failure, attempt a read-only remount. If that fails too, record it
   and continue.
6. The root is never unmounted, but is remounted read-only at the end. A
   failure there is recorded and does not stop step 8.

## 12.2.8 Step 8: Sync and the final action

This step is irreversible. Cleanup failures retained from steps 6 and 7
affect diagnostics only and never block it.

1. `sync()`. Called on all three paths — graceful, forced and
   Critical reboot.
2. `reboot(2)` with `RB_POWER_OFF`, `RB_AUTOBOOT` or `RB_HALT_SYSTEM`.
3. If `reboot(2)` returns, the final action failed. peinit enters a
   minimal failed-shutdown state, keeps PID 1 alive, records the
   failure, and retries the same action no more than once a second. It
   does not restart services and does not enter recovery mode —
   finalisation has begun and there is nothing to go back to.

## 12.2.9 Shutdown during boot

A shutdown requested while Phase 2 is still running takes effect
immediately, through the same classification: Starting services are
SIGKILLed, services that reached Active are stopped gracefully, and the
boot is abandoned.

---

# 12.3 Signals

_Peios / Advanced Peios / peinit / Shutdown_

> Every signal is blocked and read through a signalfd from the event loop — the setup, the signals handled, and reaping.

PID 1 handles every signal through a signalfd. All signals are blocked
and read from the event loop, so there are no signal handlers and no
async-signal-safety concerns anywhere in peinit.

## 12.3.1 Setup

peinit builds a mask containing every blockable signal in the supported
range — SIGKILL and SIGSTOP are not blockable and are never delivered
through a signalfd — and installs it with
`rt_sigprocmask(SIG_BLOCK, ...)` **before** entering the main event
loop. It then creates the descriptor with `signalfd4(-1, mask, ...)`,
using the same mask, with `SFD_CLOEXEC | SFD_NONBLOCK`.

If any part of that fails — blocking, creating the descriptor,
retaining it, registering it with the event loop — peinit fails closed.
There is no fallback to asynchronous handlers, because a PID 1 with
handlers installed where it expected a signalfd is a PID 1 whose
assumptions about what can interrupt it are wrong.

The mask is inherited across fork, which is why every child resets it
before exec (§5.4).

## 12.3.2 The signals

| Signal | Behaviour |
|---|---|
| SIGCHLD | Reap children with `waitpid`. Match them to tracked jobs; also reap orphans belonging to nothing. |
| SIGINT | Reboot. Three within five seconds forces one. |
| SIGTERM | Poweroff. |
| SIGPWR | Poweroff. |
| SIGHUP | Ignored. PID 1 has no controlling terminal. |
| SIGPIPE | Ignored. A broken pipe on the control socket cannot be allowed to kill PID 1. |

Everything else is ignored. The kernel protects PID 1 from fatal
signals, so no signal can kill it.

## 12.3.3 Reaping

peinit reaps with `waitpid(-1, ..., WNOHANG)` in a drain loop, and
normalises the wait status before any service or job policy sees it:

- An exited child carries its exact exit code, 0–255.
- A signalled child carries the terminating signal number and whether
  the core-dump bit was set.
- A stopped or continued status is invalid on this path, because peinit
  never asks for them. Observing one **fails closed** rather than being
  interpreted.

That last rule matters more than it looks. A stopped child reported as
an exit would be read as a service that had terminated, and peinit would
act on a process that is merely paused.

As PID 1, peinit also reaps processes nobody is tracking — orphans
reparented to it — and reports them as untracked rather than trying to
attribute them to a service.

---

# 12.4 Finalisation

_Peios / Advanced Peios / peinit / Shutdown_

> The three shutdown paths that reach the kernel and how much work each does on the way, including what survives a failure.

Three shutdown paths reach the kernel, and they do different amounts of
work on the way.

| Path | Stop waves | Seed save | Unmount | Sync | Final action |
|---|---|---|---|---|---|
| Graceful | Yes | Yes | Yes | Yes | Yes |
| Forced — three SIGINTs | No, SIGKILL everything | No | No | Yes | Yes |
| Critical service failure | No | No | No | Yes | Yes |

The two abrupt paths are required to reach `sync()` and the final kernel
action with minimal additional work, and skipping the seed and the
unmounts is what "minimal" means. A machine that is rebooting because
its audit daemon died has nothing to gain from a tidy unmount and
something to lose from the time it takes.

## 12.4.1 What survives a failure

Steps 6 and 7 — the seed and the unmounts — retain their failures as
evidence rather than acting on them. Every one is recorded, none of them
blocks step 8, and none of them enters recovery mode. By this point
there is nothing to recover *to*: services have stopped and the
filesystems are on their way down.

## 12.4.2 If the final action returns

`reboot(2)` does not return on success. If it does, the final action
failed, and peinit enters a minimal failed-shutdown state:

- PID 1 stays alive. It has to — PID 1 exiting panics the kernel.
- The failure is recorded.
- The same action is retried, no more than once a second.
- Services are not restarted, and recovery mode is not entered.

There is no way back from here. Finalisation has begun, the services are
gone and the filesystems are read-only; the only correct behaviour is to
keep trying the one thing that would end it.

`RB_HALT_SYSTEM` does not return either, so this state is only reachable
for a genuine failure rather than for the halt case.

## 12.4.3 A note on the mount table

The unmount step re-reads `/proc/self/mountinfo` when checking whether a
mount point that returned `ENOENT` or `EINVAL` is really gone. Depth
ordering puts the depth-one mounts last, alphabetically — `/dev`,
`/proc`, `/run`, `/sys` — so `/proc` is unmounted before `/run` and
`/sys` are attempted, and the check for those two cannot read the file
it needs. The result is a recorded cleanup failure and a pointless
read-only remount attempt at the tail of every graceful shutdown.

---

# 13.1 Trust Boundaries

_Peios / Advanced Peios / peinit / Security_

> The two boundaries peinit sits at — the kernel handing it a SYSTEM token, and peinit handing identity to services.

peinit sits at two.

## 13.1.1 Kernel to peinit

peinit is the first userspace process, and the kernel gives it a SYSTEM
token — `S-1-5-18`, every privilege. This is the root of trust for all
userspace identity on the system.

peinit does not drop that token and does not authenticate to anything.
Its identity is axiomatic: there is no authority above it in userspace
that could vouch for it, and the kernel handing it the boot token *is*
the vouching.

## 13.1.2 peinit to services

peinit creates service processes with specific identities and reduced
privileges. The trust runs one way. peinit trusts the kernel because it
has no alternative; services trust peinit because peinit gave them their
identity. Services do not trust each other — KACS mediates every
access between them, and nothing peinit does creates a relationship
between two services beyond the ordering their definitions asked for.

## 13.1.3 The TCB

peinit is part of the Trusted Computing Base, alongside the kernel,
KACS, LCS, KMES, registryd, authd, lpsd and eventd. A compromise of any
of them compromises the system.

That list is not decoration. It is why registryd is exempt from the
global environment layer (§5.5) — a component in the TCB cannot be
configurable by a mechanism it is itself the enforcement point for — and
it is why eventd is Critical, since a TCB whose audit trail can be
silently stopped is not one.

## 13.1.4 Filesystem enforcement

KACS enforces Security Descriptors on filesystem access. peinit's Phase
1 descriptor seeding (§2.3) exists precisely because of it: a freshly
mounted tmpfs carries no descriptor, and under `DENY_MISSING` every
inode on it would be unreachable to everything — including peinit —
until something stamps a descriptor it can inherit from.

That enforcement has no bypass. There is no owner exemption, no
privilege that overrides a missing descriptor, and no root escape, which
is what makes the seeding step fatal on failure rather than advisory.

FACS extends the model further, and until it lands the filesystem layer
still relies partly on conventional trust — correct packaging,
controlled binary paths — for the objects nothing has stamped.

---

# 13.2 peinit's Privileges

_Peios / Advanced Peios / peinit / Security_

> peinit runs as SYSTEM with every privilege, and the much narrower set it actually exercises.

peinit runs as SYSTEM with every privilege for the lifetime of the
system. What it actually exercises is narrower.

| Privilege or capability | Used for |
|---|---|
| `SeCreateTokenPrivilege` | Minting SYSTEM tokens for platform services during bootstrap, before authd exists (§4.2). |
| `SeTcbPrivilege` | Requesting tokens from authd on a service's behalf, and installing a primary token on a child whose identity differs from peinit's own. |
| Process creation | Fork and exec, inherent to PID 1. |
| cgroup management | Creating and destroying trees under `/sys/fs/cgroup/peinit/`. |
| Signal delivery | SIGTERM and SIGKILL to managed processes. |
| Mount operations | The Phase 1 virtual filesystems. |

peinit does not verify at startup that it holds any of these. A missing
`SeCreateTokenPrivilege` surfaces as an `EPERM` from the first token
mint, which is the first service start.

`SeImpersonatePrivilege` is not used. peinit passes the peer's token
descriptor to AccessCheck directly rather than impersonating the caller
and evaluating as them, so the privilege that would be needed to
impersonate is not needed at all.

For non-platform services peinit creates no tokens. It installs the ones
authd minted. It mints only for the SYSTEM platform services it starts
during bootstrap, before authd is available.

> [!NOTE]
> Minting is interim. The intended model derives a service's token from
> peinit's own handle through a KACS duplicate-with-additions operation
> — kernel-attested as a descendant of peinit's real token, and needing
> no minting privilege. That would let peinit drop
> `SeCreateTokenPrivilege` entirely, which is the point: the privilege
> to mint arbitrary tokens is the strongest thing peinit holds and the
> one it has the least use for.

---

# 13.3 The Attack Surface

_Peios / Advanced Peios / peinit / Security_

> What the socket descriptors mean in practice, and what the autorun directory exposes.

| Surface | Reachable by | Controls | Protected by |
|---|---|---|---|
| Control socket | Anything that can connect | Service lifecycle, shutdown | The socket inode's descriptor, then the peer token and AccessCheck against the target's descriptor |
| Notification socket | Anything that can connect | Service readiness, watchdog, stored descriptors | The socket inode's descriptor, then PID matching verified through a pidfd, plus the start generation |
| Registry keys | Anything with registry access | Definitions, triggers, configuration | Registry key descriptors, enforced by LCS |
| `Machine\System\Init\EnvVars\` | Anything with registry access | The environment of every service | That key's descriptor, and nothing else — variable names are not filtered |
| Service cgroups | peinit | Process tracking and clean kill | Ownership of the hierarchy |
| Phase 1 mounts | peinit | Virtual filesystem availability | Hardcoded, no external input |
| `/lcl/policy/autorun.d` | Whatever can write it | Arbitrary code as SYSTEM in early boot | The directory's descriptor |
| Boot attempt counter | Whatever can write `/.peinit/` | Recovery mode entry | That file's descriptor |
| JFS device | Whatever holds the submission privilege | Ad-hoc job submission | A KACS privilege check, kernel-enforced |

## 13.3.1 What the socket descriptors mean in practice

Both sockets inherit the single-entry descriptor peinit stamps on `/run`
in Phase 1 (§2.3), which grants `GENERIC_ALL` to SYSTEM and names no
other principal.

So both are reachable by SYSTEM alone. A connection from a
non-SYSTEM principal is refused at `connect()`, by the filesystem, before
peinit ever obtains a peer token — which means the `ACCESS_DENIED` path,
the audit event, and the Administrators entries in both default
descriptors (§4.6, §4.7) are unreachable for such a caller.

Since every service currently receives a SYSTEM token (§4.3), nothing
observes this today: every notifier and every client is SYSTEM. The two
have to move together, because a service correctly resolved to a
non-SYSTEM identity could not reach the notification socket to report
that it had started.

## 13.3.2 The autorun directory

The Phase 1 autorun step (§2.3) executes every file in
`/lcl/policy/autorun.d` as SYSTEM, before path provisioning and before
any service. It is fail-open by design, so nothing about a script going
wrong stops the boot.

Its only protection is the descriptor on that directory. Anything that
can write there executes as SYSTEM at the earliest point in userspace
that exists.

---

# 13.4 Security Invariants

_Peios / Advanced Peios / peinit / Security_

> The properties peinit does not violate, and the code paths that make each of them structural.

Properties peinit does not violate.

**1. peinit does not grant privileges it was not asked to grant.**
`RequiredPrivileges` is subtractive. peinit removes privileges and never
adds one — there is no code path that constructs anything but a removal
(§4.5).

**2. peinit does not bypass AccessCheck for control operations.** Every
control command reaches a check against the appropriate descriptor.
There is no backdoor, no override flag, and no "trust localhost".

**3. peinit does not expose one service's state to another without
access control.** `list` returns only what the caller may query, and
`status` is checked per service (§10.2).

**4. peinit records every access denial.** A failed AccessCheck produces
an `access.denied` event carrying the caller's SID, the target, the
requested right by name, and the access bits requested and granted.
Silent denial is not acceptable.

**5. The control descriptor and the ServiceSecurity descriptors are the
only policy inputs for runtime access control.** peinit consults no
configuration file, no environment variable and no hardcoded principal
list. The only inputs to AccessCheck are those two descriptors, sourced
from the registry with a compiled-in default.

**6. peinit does not share its SYSTEM token.** It opens its own token
query-only, as a template, and never installs it on a child. Even an
`Identity=SYSTEM` service gets a separately minted token.

**7. peinit does not drop its SYSTEM identity.** PID 1 runs as SYSTEM
for the lifetime of the system. Only the forked child installs a token;
peinit never installs one on itself.

**8. Identity is deterministic, and the dangerous case is never
implicit.** Every service runs with a known identity. `SYSTEM` has to be
declared explicitly; an absent or empty `Identity` means `LocalService`.

The declaration logic upholds the eighth: an empty value resolves to
`LocalService`, and `SYSTEM` is reached only by naming it. What a
service actually receives depends on materialisation, and while the
authd client returns a minted SYSTEM token for every identity (§4.3), a
service that declared nothing runs on one.

---

# 14.1 Losing the Registry

_Peios / Advanced Peios / peinit / Failure Modes_

> peinit depends on the registry once, hard, and then never the same way again — what losing it does in each phase and at runtime.

peinit depends on the registry once, hard, and then never again in the
same way. The difference between those two situations is most of what
this section is about.

## 14.1.1 During Phase 1

registryd failing to start, failing readiness, or failing the
schema-version probe sends peinit to recovery mode immediately. There is
no Phase 2 without a registry and nothing useful to degrade to. The
counter is incremented, so a persistently broken registryd burns boot
attempts even though every one of them fails the same way.

The recovery shell reached from a Phase 1 failure does not have
registryd running, and the offline tools (§2.8) are what an
administrator has.

## 14.1.2 During Phase 2

A registry read that fails or times out during the definition read sends
peinit to recovery, for the same reason: there is no graph to boot.

## 14.1.3 At runtime

This is where the design pays off. peinit holds a complete in-memory
model and does not read the registry during normal supervision, so
registryd going away does not stop peinit supervising anything. Services
keep running, restarts keep working, the control socket keeps answering,
and timers keep firing.

What stops working is anything that needs new configuration:
reload-config fails, change notifications stop arriving, and a timer's
last-run timestamp cannot be written — so a persistent timer may produce
a spurious catch-up on the next boot.

registryd itself is a Critical service, so its failure takes the
ordinary Critical path: restart budget, then sync and reboot. peinit
does not have to handle a permanently absent registryd at runtime,
because the system reboots first.

## 14.1.4 A definition that will not decode

One definition that fails to decode fails that definition (§3.2). At
boot the key is marked Failed with cause `ValidationError` and every
other service starts normally; on a reload the whole reload is rejected
and the previous generation is left in place.

Both are the safe answer for their caller. A reload is atomic and has a
working configuration behind it, so refusing the change costs nothing. A
boot has no previous generation to fall back to, so refusing everything
would mean not booting at all over a single malformed key — which is
what it used to do.

---

# 14.2 Losing a Dependency

_Peios / Advanced Peios / peinit / Failure Modes_

> What breaks when authd, eventd or JFS is unavailable, and what a bound dependency that never becomes satisfying does.

## 14.2.1 authd

Without authd, no non-SYSTEM service can obtain a token, so every such
start fails with `ParentSetupFailure`. Platform services are unaffected,
since they never take that path.

authd is Critical, so its own failure eventually reboots the system
rather than leaving it in a state where no user-facing service can
start.

## 14.2.2 eventd

peinit supervises eventd like anything else, and eventd is Critical.
While it is down, peinit re-enables the pre-eventd buffer (§11.2) and
keeps collecting output; when eventd returns the handoff repeats.

There is a log gap bounded by the buffer size. There is **no** event gap
— audit events land in the KMES ring buffer regardless of eventd's
state, and eventd resumes from the last persisted sequence.

## 14.2.3 JFS

The device may not exist. Phase 1 treats a failure to open `/dev/jfs` as
a warning and continues, and the boot is unaffected. Nothing else in
peinit depends on it.

## 14.2.4 A bound dependency

A service that `BindsTo` something is stopped when that something stops,
with cause `BindsToPropagation`, and restarted when it returns, with
cause `BindsToRecovery` and no charge against the restart budget
(§7.1). This is the one dependency relationship that recovers by itself.

A `Requires` dependency crashing does not affect a running dependent at
all. The dependent was ordered after it, not coupled to it.

## 14.2.5 A dependency that never becomes satisfying

A dependent blocked on a hard dependency waits until its own operation
lifetime expires, and then fails. The timeout is measured from when the
operation was created rather than from when it started running, so a
service queued behind a slow dependency can exhaust its `StartTimeout`
without ever having attempted to start — which is the honest answer, in
that the caller really has been waiting that long.

---

# 14.3 Unkillable Processes

_Peios / Advanced Peios / peinit / Failure Modes_

> A process in uninterruptible sleep ignores SIGKILL — what Abandoned means, and the generational escape that lets peinit move on.

A process in uninterruptible kernel sleep does not respond to SIGKILL.
Nothing peinit can do will make it exit, and the design consequence is
that peinit stops trying rather than blocking on it.

Detection is uniform: after sending the kill, arm a post-kill deadline
(5 seconds by default) and check whether the cgroup still reports as
populated when it fires.

What follows depends on which cgroup it was:

| Cgroup | Consequence |
|---|---|
| `main/` | The service goes to Abandoned with cause `ProcessUnkillable`. Supervision stops; the cgroup is leaked. |
| `health/` or `hooks/` | The sub-cgroup is orphaned and recorded as a leak. The service carries on normally. |
| `checks/` | The sub-cgroup is dropped with no record. |

The distinction is about what the stuck process is holding. A main
process holds the service's ports, locks and connections, so a service
whose main process cannot be killed cannot be restarted into a working
state. A health check or a hook holds nothing, so a stuck one is a
nuisance rather than a blocker.

## 14.3.1 What Abandoned means

peinit has given up. The service is not restarted, does not satisfy
dependents, and every lifecycle command against it is invalid except
`reset` (§10.3).

A `reset` re-checks `main/`. If it has finally emptied — the I/O that
was hung completed, or the device came back — peinit cleans up the whole
tree and the service returns to Inactive. If it is still populated, the
service returns to Inactive anyway, the cgroup stays leaked, and both the
acknowledgement and the console carry a warning saying so.

## 14.3.2 The generational escape

A leaked cgroup cannot be removed, so the next start would collide with
it. Recording a leak increments the service's cgroup generation, and the
next start builds a fresh tree at `.gen<N>` (§5.1). Old trees persist
until reboot.

That is what lets a service recover from a leak at all: peinit cannot
clean up the old tree, so it stops trying to and uses a new one.

## 14.3.3 What it actually means

Every path here has the same underlying cause. Something below the
service — a hung mount, a failing controller, a device that stopped
answering — is not responding to the kernel, and no amount of restarting
the service will change that. The leak record exists to say so, because
the alternative is a service that mysteriously will not restart.

---

# 14.4 Resource Exhaustion

_Peios / Advanced Peios / peinit / Failure Modes_

> Descriptors, processes, memory and disk running out — how each reaches single-threaded PID 1, and what the OOM killer does.

peinit is single-threaded PID 1, so most resource pressure reaches it as
a failure at a syscall rather than as slowness.

## 14.4.1 Descriptors

peinit holds a descriptor per supervised process (a pidfd), two per
service for output pipes, one per armed timer, one per control
connection, plus the sockets, the epoll instance, the signalfd and the
JFS device.

`EMFILE` or `ENFILE` from `pipe2` or `clone3` fails the start with
`ParentSetupFailure` — a restart-eligible cause, so a service that
failed because the system was momentarily out of descriptors gets
another go.

Two paths leak descriptors slowly: the pre-start check helper's result
descriptor and pidfd are unregistered from the event loop but not
closed, so a service using filesystem conditions leaks two per start.

## 14.4.2 Processes

`EAGAIN` from `clone3` — the PID limit — is also `ParentSetupFailure`
and restart-eligible.

The one path that leaks processes is the timer last-run write, which
forks a child per firing (§9.2). The children are short-lived and reaped
by the ordinary PID 1 reaper, but the fork is real and happens on every
firing of every persistent timer.

## 14.4.3 Memory

`ENOMEM` from `clone3` behaves like the others.

peinit's own memory is bounded by design in the places that could
otherwise grow without limit: the pre-eventd buffer has a fixed size and
drops its oldest, there is no outbound queue for log delivery, terminal
jobs and operations are dropped rather than retained, and neither has a
history structure.

Two things do accumulate. Graph execution contexts and their operation
associations are never retired, so each boot and each on-demand start
adds one for the life of the process. And an `OnFailure` chain entry for
a handler that starts and stays running is never cleared, so it
permanently occupies a slot of that failure's depth budget.

## 14.4.4 Disk

A full root filesystem shows up in three places. The boot attempt
counter cannot be written, which peinit treats as a counter of zero and
continues — a failure to record an attempt is not itself worth
escalating. The random seed cannot be saved at shutdown, which is
recorded and does not block the shutdown. And registryd cannot write,
which is registryd's problem and reaches peinit as a Critical service
failing.

## 14.4.5 The OOM killer

An `ErrorControl=Critical` service is marked OOM-immune, with
`oom_score_adj` at `-1000`; everything else is left at the default
(§5.4). A Critical service is one whose loss reboots the machine, so
letting the OOM killer pick it would turn memory pressure into a reboot.

peinit itself is PID 1 and the kernel will not choose it.

---

# 14.5 Power Loss and Corruption

_Peios / Advanced Peios / peinit / Failure Modes_

> The three things peinit writes and how each is made to survive a power loss — plus damaged files, timers, and an interrupted shutdown.

## 14.5.1 What peinit writes

Three things, and all three are written to survive an unexpected loss of
power:

| What | Where | How |
|---|---|---|
| Boot attempt counter | `/.peinit/boot-attempts` | A plain integer, rewritten each boot. |
| Local machine ID | `/lcl/etc/machine-id` | Temporary file, flush, rename. |
| Random seed | `/var/state/peinit/random-seed` | Temporary file on the same filesystem, flush, atomic rename, directory flush. |

The seed and the machine ID are atomic in the sense that matters: a
reader sees either the old value or the new one, never a partial write.

## 14.5.2 Reading a damaged file

The three behave differently on finding something they cannot use, and
the differences track how bad each situation is.

**The counter** is the strictest. Absent means zero. But unreadable,
empty, non-decimal, carrying trailing data, or overflowing all send
peinit to recovery mode. A counter that cannot be trusted cannot
escalate, and the whole point of it is escalation — so failing to read
it is treated as though it had already escalated.

**The machine ID** is regenerated. Absent, empty, all zeroes, the wrong
length, not hexadecimal, or missing its trailing newline all produce a
fresh identifier, recorded as a warning. It is an opaque install
identifier, not a security principal, and a new one is a smaller problem
than no boot. An I/O failure while reading, generating, or writing does
send peinit to recovery.

**The seed** is entirely fail-soft. Absent, empty, oversized, or
unrestorable all continue the boot silently. A system with no entropy
cache still boots; it starts with less entropy, which is the image
builder's problem to solve with a hardware or virtio RNG.

## 14.5.3 The registry

peinit does not write service state to the registry, apart from timer
last-run timestamps. Everything else about a service's runtime state
lives in memory and is rebuilt from the definitions on the next boot,
which means a power loss cannot leave peinit's own state inconsistent —
there is no state on disk to be inconsistent with.

Registry consistency across power loss is loregd's concern, and its
recovery paths are what the recovery shell offers (§2.8).

## 14.5.4 Timers across a loss

A persistent timer's last-run timestamp is written after the firing and
after the start is initiated, not after the service completes (§9.3). A
power loss mid-run therefore does not re-trigger on the next boot — the
run was attempted, not missed.

A power loss between the firing and the write does re-trigger, which is
the right way round: one extra run is better than a silently skipped
one.

## 14.5.5 Shutdown that never finishes

If power is lost during the graceful sequence, the effect depends on how
far it got. Before step 7, the filesystems are still mounted read-write
and the next boot behaves like any unclean shutdown. After step 7, the
root has been remounted read-only and everything else unmounted, so
there is nothing outstanding to lose.

The counter was incremented at the start of the boot that is now ending,
and is reset only by a *successful* boot — so a power loss during a
shutdown leaves the counter advanced. Enough of them in a row reach the
recovery threshold, which is the intended behaviour: a machine that
keeps losing power mid-shutdown is a machine an administrator should be
looking at.

---

# Appendix A Registry Key Reference

_Peios / Advanced Peios / peinit_

> Every registry key peinit reads or writes — service definitions, boot configuration, operational parameters and the watches it holds.

Every registry key peinit reads or writes. Semantics are in the sections
referenced.

## A.1 Service definitions

| Key | Purpose | Defined in |
|---|---|---|
| `Machine\System\Services\` | Parent key. Each child key is one service. | §3.2 |
| `Machine\System\Services\SchemaVersion` | Schema version guard. `REG_DWORD`, currently 1. Created by peinit if absent. | §2.3, §3.2 |
| `Machine\System\Services\ServiceSecurity` | The descriptor inherited by definitions that carry none. `REG_BINARY`. | §4.6 |
| `Machine\System\Services\<name>` | One service definition. | §3.2 |
| `Machine\System\Services\<name>\LastTimerRun` | Last-run timestamp for a single-trigger persistent timer. `REG_QWORD`, written by peinit. | §9.3 |
| `Machine\System\Services\<name>\TimerState\` | Per-trigger timestamps for a multi-trigger service. Each value is named by the percent-encoded schedule and holds a `REG_QWORD`. | §9.3 |

## A.2 Boot configuration

| Key | Type | Default | Purpose | Defined in |
|---|---|---|---|---|
| `Machine\System\Boot\MaxParallelStarts` | dword, > 0 | 10 | Services starting concurrently during boot. Zero, a type mismatch, or a malformed payload is invalid and enters recovery. | §2.5 |
| `Machine\System\Boot\BootSuccessGrace` | dword | 30 | Seconds every Critical service has to hold a dependent-satisfying state before the boot counts as successful. | §2.5 |
| `Machine\System\Boot\SettleTimeout` | dword | 5 | Seconds to wait for the boot set to settle before starting `boot:settled` services regardless. Zero is legal. | §2.5 |
| `Machine\System\Boot\ShutdownTimeout` | dword | 90 | Seconds for the entire graceful shutdown. | §12.2 |
| `Machine\System\Boot\PostKillTimeout` | dword | 5 | Seconds for a cgroup to drain after SIGKILL before it is treated as stuck. Bounds one service's final stop, where `ShutdownTimeout` bounds the sequence. | §12.2 |

## A.3 Operational parameters

| Key | Type | Default | Purpose | Defined in |
|---|---|---|---|---|
| `Machine\System\Init\ControlSecurity` | binary | SYSTEM and Administrators, both rights | The descriptor for system-level control operations. | §4.7 |
| `Machine\System\Init\MaxControlConnections` | dword | 32 | Concurrent control socket connections. | §10.1 |
| `Machine\System\Init\MaxRequestSize` | dword | 65536 | Maximum control request size, in bytes. | §10.1 |
| `Machine\System\Init\ConnectionTimeout` | dword | 30 | Seconds before an idle control connection is closed. | §10.1 |
| `Machine\System\Init\MaxLogLineLength` | dword | 8192 | Bytes per output line before truncation. Minimum 256; below that the default is used and a warning logged. | §11.3 |
| `Machine\System\Init\MaxLogBufferPerService` | dword | 65536 | Pipe capacity per service, applied with `F_SETPIPE_SZ`. Minimum 4096 (one page, the kernel's own floor). | §11.3 |
| `Machine\System\Init\LogReadBytesPerEvent` | dword | 16384 | Bytes drained from one output pipe per readable event. Minimum 512. | §11.3 |
| `Machine\System\Init\PreEventdBuffer` | dword | 1048576 | Bytes of output retained before eventd is available. Applied at boot and on reload. Minimum 4096. | §11.2 |
| `Machine\System\Init\EnvVars\` | parent key | empty | Variables injected into every service but registryd. Value name is the variable name; `REG_SZ` data is the value. Its descriptor is security-critical. | §5.5 |
| `Machine\System\Init\ProvisionedPaths\` | parent key | empty | Boot-time path provisioning entries. | §2.4 |
| `Machine\System\Init\ProvisionedPaths\<name>\Kind` | string | — | `directory` or `file`. Required. | §2.4 |
| `Machine\System\Init\ProvisionedPaths\<name>\Path` | string | — | The absolute path. Required. | §2.4 |
| `Machine\System\Init\ProvisionedPaths\<name>\Security` | binary | built-in | The descriptor to apply. | §2.4 |
| `Machine\System\Init\ProvisionedPaths\<name>\Required` | dword | 0 | If 1, a failure enters recovery before Phase 2. | §2.4 |

## A.4 Other subsystems

| Key | Type | Purpose | Defined in |
|---|---|---|---|
| `Machine\System\eventd\LogSocketPath` | string | Where peinit forwards service output. | §11.4 |

## A.5 Watches

peinit subscribes to `Machine\System\Services\` and
`Machine\System\Init\` at boot. Any drained event triggers a full
reload-config, which also covers the OVERFLOW case (§3.7).

---

# Appendix B Constants and Paths

_Peios / Advanced Peios / peinit_

> Every value compiled into peinit that no registry key changes — paths, timeouts, limits, environment and the descriptors it applies.

Values compiled into peinit, which no registry key changes.

## B.1 Filesystem paths

| Path | Purpose |
|---|---|
| `/usr/bin/peinit2` | Where peinit is installed in package storage. |
| `/bin/peinit2` | The runtime path the kernel `init=` names. |
| `/sbin/registryd` | The compiled-in registryd image path. |
| `/var/state/loregd/Machine.hive` | The machine hive, passed to registryd. |
| `/var/state/loregd/Users.hive` | The users hive, passed to registryd. |
| `/.peinit/` | peinit's own state directory on the root filesystem. |
| `/.peinit/boot-attempts` | The boot attempt counter. |
| `/lcl/etc/machine-id` | The local machine identifier. |
| `/var/state/peinit/random-seed` | The persisted entropy seed. |
| `/lcl/policy/autorun.d` | Phase 1 autorun scripts. |
| `/run/services/peinit/control.sock` | The control socket. |
| `/run/services/peinit/notify.sock` | The notification socket, by default. |
| `/sys/fs/cgroup/peinit/` | The root of every service cgroup tree. |
| `/dev/jfs` | The job forwarding device. |
| `/dev/rtc`, `/dev/rtc0` | The hardware clock, in that order of preference. |
| `/dev/console` | Where peinit writes its own messages. |
| `/bin/recsh`, `/bin/sh` | The recovery shell, in that order of preference. |

## B.2 Timeouts and limits

| Constant | Value | Meaning |
|---|---|---|
| registryd setup timeout | 30 s | Process setup, driven synchronously in Phase 1. |
| registryd readiness timeout | 30 s | Waiting for `READY=1` in Phase 1. |
| Reload detection window | 2 s | Waiting for `RELOADING=1` after a reload signal. Bounded above by the operation deadline. |
| Restart delay cap | 60 s | The ceiling on exponential backoff. |
| Timeout extension cap | ×4 | The multiple of a phase's base timeout an extension may reach. |
| Operation retention | 60 s | How long a terminal operation is kept before being dropped. |
| Boot attempt threshold | 3 | The default, overridden by `peios.bootattempts=`. |
| `OnFailure` chain depth | 16 | The maximum handler chain from one originating failure. |
| Pre-eventd buffer | 1 MiB | The compiled-in buffer capacity. |
| Notification datagram | 64 KiB | The receive buffer size. |
| Descriptors per datagram | 64 | The control message buffer capacity. |
| Random seed | 512 bytes | Written at shutdown. Restoring accepts 1–4096 bytes. |
| Control listen backlog | 32 | |
| First injected descriptor | 3 | Where stored descriptors are placed. |
| Final action retry | 1 s | The minimum interval between `reboot(2)` retries. |

## B.3 Environment

| Variable | Value |
|---|---|
| `PATH` | `/sbin:/bin` — the compiled-in base for every service. |

The recovery shell additionally receives `TERM=linux` and `HOME=/`.

## B.4 Kernel command line

| Parameter | Effect |
|---|---|
| `peios.safemode=1` | Force Safe mode. |
| `peios.recovery=1` | Force recovery mode. |
| `peios.bootattempts=N` | The recovery threshold; `0` disables the check. |
| `peios.quiet=N` | Console verbosity: 0, 1 or 2. |
| `peios.notifysocket=PATH` | Override the notification socket path. |

## B.5 Descriptors peinit applies

| Where | SDDL |
|---|---|
| `/dev/shm`, `/run`, `/sys/fs/cgroup` after mounting | `O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)` |
| `/dev/null`, `/dev/zero`, `/dev/full`, `/dev/random`, `/dev/urandom`, `/dev/tty`, `/dev/ptmx` (DACL only) | `D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FRFW;;;WD)` |
| A provisioned path with no `Security` | `O:SYG:SYD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FR;;;BU)` |
| A service's `/run/<name>` runtime directory | `O:SYG:SYD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;<service SID>)` |
| The random seed file | `O:SYG:SYD:(A;;GA;;;SY)` |
| The default ServiceSecurity | `O:SYG:SYD:(A;;GA;;;SY)(A;;0x0005;;;BA)` |
| The default ControlSecurity | `O:SYG:BAD:(A;;0x0003;;;SY)(A;;0x0003;;;BA)` |

## B.6 Access rights

| Right | Bit |
|---|---|
| `SERVICE_QUERY_STATUS` | 0x0001 |
| `SERVICE_START` | 0x0002 |
| `SERVICE_STOP` | 0x0004 |
| `SERVICE_INTERROGATE` | 0x0008 |
| `SERVICE_ALL_ACCESS` | 0x000F |
| `SYSTEM_SHUTDOWN` | 0x0001 |
| `SYSTEM_RELOAD_CONFIG` | 0x0002 |

## B.7 Identifiers

Every GUID peinit generates — jobs, operations — is UUIDv7, so
identifiers sort by creation time.
