# Developing for Peios

> How to build software for Peios.

---

# Debugging the kernel

_Peios / Developing for Peios / Debugging the kernel_

> The PKM subsystems — KACS, KMES, LCS — expose static tracepoints to the standard Linux tracing stack of ftrace, perf, and eBPF.

Most questions about *why* the Peios kernel did something — why an access was denied, why an event never reached userspace, why a registry lookup stalled — are answered from inside the kernel, before any userspace tool can see the state involved. Peios makes that interior observable through the same tracing infrastructure the rest of the Linux kernel uses: **static tracepoints**.

The PKM security subsystems each expose a tracepoint system:

- **`kacs:`** — the access-control decisions. Every instrumented KACS hook records its verdict (allow/deny), the object it acted on (inode number and superblock magic — never a pathname), and a `reason` code naming the exact return path it took. This is where "why was this denied?" is answered.
- **`kmes:`** — the health of the event substrate: ring-buffer drops, capacity swaps, rate-limit throttling, backpressure. These trace the *machinery*, not the security events KMES ships to userspace (those are the [event stream](/peios/security-fundamentals/inspecting/the-event-stream.md)).
- **`lcs:`** — the registry source device: request/response round-trips, timeouts, transaction state transitions, source mark-downs.

Because these are ordinary kernel tracepoints, everything the kernel's tracing stack can do applies unchanged: enable individual events or whole subsystems, attach ftrace filters and triggers, sample with perf, or attach eBPF programs. They cost nothing when disabled (a patched-out branch) and record structured fields rather than formatted text, so you filter on `ret`, `reason`, or an inode number directly.

The [Kernel tracepoints](/peios/developing-for-peios/debugging-the-kernel/kernel-tracepoints.md) page covers how to enable them — at runtime through `tracefs`, or from the very first moments of boot through the kernel command line — and how to read what they emit.

> [!NOTE]
> **Safety.** PKM tracepoints never record pathnames or security-descriptor bytes. They carry inode numbers, superblock magics, resolved policies, access masks, numeric identifiers, lengths, and reason codes only. This is by design, so they are safe to leave compiled into production kernels and enable in the field.

---

# Kernel tracepoints

_Peios / Developing for Peios / Debugging the kernel_

> Enabling and reading the kacs:, kmes:, and lcs: tracepoint systems — at runtime through tracefs, at boot through the kernel command line.

PKM exposes its security subsystems through three tracepoint systems — `kacs:`, `kmes:`, and `lcs:` — registered with the standard Linux tracing infrastructure. This page shows how to turn them on and read them.

## Discovering the events

Every event and its fields are self-describing through `tracefs`. The live catalog is authoritative — prefer it over any static list:

```sh
# every PKM tracepoint
ls /sys/kernel/tracing/events/kacs /sys/kernel/tracing/events/kmes /sys/kernel/tracing/events/lcs

# the fields (and their symbolic decodings) of one event
cat /sys/kernel/tracing/events/kacs/kacs_file_access/format
```

The numeric `reason`, `op`, and `state` codes carried by these events are a stable, append-only diagnostic ABI defined in `<pkm/trace.h>`; the `format` file maps them back to their symbolic names for you.

## Enabling at runtime

Enable a whole subsystem, or a single event:

```sh
cd /sys/kernel/tracing

echo 1 > events/kacs/enable                 # all KACS access-decision events
echo 1 > events/kacs/kacs_file_access/enable # just one

cat trace                                    # read what has accumulated
echo 1 > tracing_on                          # (on by default)
```

Because the fields are structured, you filter in the kernel rather than grepping text. To see only **denials**:

```sh
echo 'ret != 0' > events/kacs/kacs_file_access/filter
```

To watch a single inode, or one `reason`:

```sh
echo 'ino == 1234' > events/kacs/kacs_file_access/filter
echo 'reason == 3' > events/kacs/kacs_sd_cache_lookup/filter   # 3 == miss-needs-synth
```

perf and eBPF attach to the same tracepoints by name — e.g. `perf record -e kacs:kacs_file_access`, or a `tracepoint:kacs:kacs_file_access` probe from `bpftrace`.

## Enabling at boot

The most common reason to reach for these is a decision that happens *before userspace exists* — the access checks that fire as the root filesystem mounts. `tracefs` is not available that early, so enable the events on the kernel command line and route them to the console with `tp_printk`:

```
trace_event=kacs:*,kmes:*,lcs:* tp_printk
```

`trace_event=` enables the listed events as the tracing subsystem initialises — which happens before the PKM LSM itself initialises, and well before the first access check — so no early decision is missed. `tp_printk` prints each enabled tracepoint to the kernel log, giving you a complete decision transcript on the console with no userspace involved. Narrow the selection (`trace_event=kacs:kacs_file_access`) to cut the volume.

> [!NOTE]
> **Migration note.** This replaces the older `kacs.trace=1` boot parameter, which emitted a single hand-rolled `pr_info` line per KACS access decision. The equivalent today is `trace_event=kacs:* tp_printk`, which produces the same pre-userspace transcript but as structured, filterable events across all three subsystems. `kacs.trace=1` is no longer recognised.

## Reading a KACS access decision

A `kacs:` access-decision event answers "what did KACS decide about this object, and why?". The key fields:

- **`verdict`** — `allow` or `deny`, derived from `ret` (`0` is allow; a negative errno is a denial). Filter on `ret` for machine use.
- **`reason`** — the specific return path taken, as a symbolic name (e.g. `decision`, `unmanaged`, `no-token`, `pip-context`). The same object can be denied for very different reasons; this names which one.
- **`ino` / `sb_magic`** — the inode number and the filesystem's superblock magic, identifying the object without disclosing its path.
- **`mount_policy`** — the resolved [mount policy](/peios/using-peios/mount-policies/overview.md) for the object's filesystem, which frequently explains a denial on an unmanaged or synthesis-only mount.
- **`access`** — the desired-access mask being checked.

For a worked example of tracing a specific denial end to end, see [Debugging a denial](/peios/security-fundamentals/access-decisions/debugging-a-denial.md).

---

# Writing regman pages

_Peios / Developing for Peios / Documenting configuration_

> How to document your package's registry keys so regman can explain them. You ship a .regman fragment in /usr/share/regman/; this is its fenced format, the fmt/lint workflow, and the rules the tools won't catch for you.

The registry stores a value's *type* and *bytes*, but never its *meaning* — it does not know that a queue depth must be at least 16, or what changing it costs. That knowledge ships with the software that owns the key, as documentation `regman` reads. From the operator's side this is [the registry manual](/peios/using-peios/registry-administration/regman.md); from yours, the package author's, it is a file you write and install.

This page is how you write one. The package that *owns* a set of registry keys is the package that documents them — the only arrangement that stays correct as packages come and go.

## Where the documentation lives

`regman` reads a drop-in directory, `/usr/share/regman/`. Each `*.regman` file in it is one **provider**, and the provider's name is the file's stem:

```
/usr/share/regman/
    kmes.regman          # provider: kmes
    exampled.regman      # provider: exampled
```

A package documents its whole registry surface in one file. There is no central index to register with and no install-time hook: dropping the file in is the whole act, and removing it on uninstall is the whole undo. A missing directory simply means nothing is documented — not an error.

You ship the fragment the same way you ship any other file. Have your build target install it, then map it into the payload from the package file's `[files]` table:

```toml
[files]
"main:share/regman/exampled.regman" = "usr/share/regman/exampled.regman"
```

If the fragment is hand-written rather than generated — which it usually is — keep it beside the recipe and take it from there, no build target involved:

```toml
[files]
"@recipe:exampled.regman" = "usr/share/regman/exampled.regman"
```

See [Packages](/pekit/recipes/packages.md) for ref resolution, and [Multi-package recipes](/pekit/recipes/multi-package.md) if one recipe produces a family of packages that each document their own keys.

> [!NOTE]
> `regman` reads shipped documentation, never the live registry. A `.regman` page describes what a setting *means and should be* — its type, default, valid range — not what some machine currently has it set to. Don't write current-state-specific prose; it won't be true on the next box.

## The shape of a fragment

A fragment is a sequence of **records**, each documenting one key or one value. A record is a fence line, a small header, a blank line, then a Markdown body:

```
--- machine\system\exampled maxqueuedepth
canonical: Machine\System\Exampled MaxQueueDepth
type: REG_DWORD
default: 1024
valid: 16–65536
applies: restart

Maximum number of jobs held in the spool queue before new submissions
are rejected with EAGAIN.

Raising this lets the queue absorb larger bursts at the cost of memory.
The queue is preallocated, so the change takes effect at the next service
restart rather than live.
```

Four parts, in order:

- **The fence line** — `--- ` (three dashes, one space) followed by the **anchor**: the case-folded, lowercased lookup token `regman` matches against. You do not write this by hand; `regman fmt` bakes it from `canonical` (see *The fmt/lint workflow*, below).
- **The header** — `key: value` lines, one per line, until the first blank line. Keys are case-insensitive; values are trimmed. Unknown keys are ignored, so a typo'd field is silently dropped rather than rejected — watch for it.
- **A blank line** — this is what ends the header and starts the body. A record with no blank line has no body.
- **The body** — Markdown, rendered when the page is shown.

### Key docs and value docs

There is no `kind:` field. A record is a **value doc** if it carries any of `type`, `default`, `valid`, or `applies`; otherwise it is a **key doc**. That is the whole distinction — a key doc is just a record that omits all four.

A **key doc** introduces a subtree. Its body explains the shared semantics once (validation philosophy, security intent, how the subsystem reads the keys), and `regman <key>` renders that body followed by an auto-generated index of the values beneath it:

```
--- machine\system\exampled
canonical: Machine\System\Exampled

The Exampled spooler reads its tuning parameters from the values under
this key. Compiled-in defaults apply until LCS is available; after that
Exampled reads, validates, and applies each value, then watches the
subtree for later changes.
```

A **value doc** documents one knob. The four value fields are what fill in its card:

| Field | On | What it tells the reader |
|---|---|---|
| `canonical` | every record | The original-case `Path[ Value]` shown in the heading. **The one required field.** |
| `type` | value docs | Registry type tag — `REG_DWORD`, `REG_QWORD`, `REG_SZ`, … |
| `default` | value docs | The value used when nothing is set. |
| `valid` | value docs | The range, set, or constraint a sensible value must satisfy. Human-readable prose. |
| `applies` | value docs | When a change takes effect: `live`, `restart`, or `reboot`. |
| `deprecated` | either | Present ⇒ the item is being retired; the value is the replacement or a note. Renders as a banner at the top of the card. |

`canonical` is the only field any record *must* have — a record without it is dropped and flagged. The four value fields are not individually enforced, but each one you omit is a blank on the card, and `applies` in particular is the field operators reach for most. Treat a value doc as incomplete until it carries all four.

> [!NOTE]
> `regman` renders field values **verbatim** — it does not reformat them. If you want the default to read `1024 (1 K jobs)`, write exactly that into the `default:` field. The card shows what you wrote.

### The body's first line is the summary

There is no `summary:` field. The **first non-empty line of the body** is taken as the one-sentence summary — it is what shows next to the value name in a key doc's index and in `regman -k` search results. So write the body like a good commit message or docstring: lead with one self-contained sentence, then elaborate in the paragraphs below.

The body is Markdown: `**bold**`, `` `code` ``, headings, and bullet lists render; prose is wrapped to the terminal width. On a non-tty (piped) it renders plain, honouring `NO_COLOR`.

## A complete fragment

`/usr/share/regman/exampled.regman`, one key doc plus one value doc, as a package would ship it:

```
--- machine\system\exampled
canonical: Machine\System\Exampled

The Exampled spooler reads its tuning parameters from the values under
this key. Compiled-in defaults apply until LCS is available.

Security is on this key, not per value: every value inherits this key's
Security Descriptor.

--- machine\system\exampled maxqueuedepth
canonical: Machine\System\Exampled MaxQueueDepth
type: REG_DWORD
default: 1024
valid: 16–65536
applies: restart

Maximum number of jobs held in the spool queue before new submissions
are rejected with EAGAIN.

Raising this lets the queue absorb larger bursts at the cost of memory.
The queue is preallocated, so the change takes effect at the next service
restart rather than live.
```

That renders, for `regman Machine\System\Exampled MaxQueueDepth`, as:

```
Machine\System\Exampled MaxQueueDepth                 documented by exampled

  Type     REG_DWORD
  Default  1024
  Valid    16–65536
  Applies  restart

Maximum number of jobs held in the spool queue before new submissions
are rejected with EAGAIN.

Raising this lets the queue absorb larger bursts at the cost of memory.
The queue is preallocated, so the change takes effect at the next service
restart rather than live.
```

— and the bare `regman Machine\System\Exampled` renders the key body plus the index:

```
Machine\System\Exampled                               documented by exampled

The Exampled spooler reads its tuning parameters from the values under
this key. Compiled-in defaults apply until LCS is available.

Security is on this key, not per value: every value inherits this key's
Security Descriptor.

Values
  MaxQueueDepth  Maximum number of jobs held in the spool queue before n…
```

## The fmt/lint workflow

You write `canonical` with whatever casing reads best. The fence anchor — the folded, lowercased token the scanner matches — is *derived* from it, because the registry compares keys case-insensitively (Unicode Simple Case Folding) while a filesystem does not. Keeping the two in sync by hand is exactly the kind of error a tool should own, so two commands own it:

```bash
regman fmt  exampled.regman    # bake the folded anchor onto every fence line
regman lint exampled.regman    # verify structure and anchors
```

The workflow is:

1. **Write each record** with its `canonical` and body. Open the record with a fence line — you can put any placeholder after the dashes, e.g. `--- x`; `regman fmt` overwrites it.
2. **Run `regman fmt`.** It rewrites every fence to `--- <fold(canonical)>` and prints `<file>: anchors updated` if anything changed. It is idempotent — a second run is a no-op. A record missing `canonical` is skipped and reported.
3. **Run `regman lint`.** It reports any record with no `canonical:` and any fence anchor that disagrees with its `canonical` (`anchor for ...: fence has X, expected Y — run regman fmt`). A clean fragment prints nothing and exits 0.

Wire `regman lint` into your build or CI so a malformed fragment fails the build rather than shipping silently broken.

### Preview before you ship

`regman` honours `REGMAN_DIR`, so you can point it at your working directory and see exactly what an operator will see, without installing anything:

```bash
REGMAN_DIR=. regman Machine\\System\\Exampled MaxQueueDepth
REGMAN_DIR=. regman Machine\\System\\Exampled
REGMAN_DIR=. regman -k queue
```

## Rules the tools won't catch

`regman lint` checks structure and anchors. These conventions it does **not** enforce — they are on you:

- **Never start a body line with `--- ` (three dashes, a space, then text).** Any such line *is* a fence — it will be read as the start of a new record and silently split your page in two. A bare `---` on its own line (a Markdown thematic break) is fine; it's the `--- text` form that bites. If you need a horizontal rule, use `***` or `___`.
- **Keep `applies` to the vocabulary `live` / `restart` / `reboot`.** A short parenthetical is fine (`live (ring-buffer swap)`); a freeform sentence defeats the at-a-glance purpose of the field.
- **Lead the body with one summary sentence** (above) — an empty or buried first line leaves a blank in the key index and in `-k` results.
- **Don't invent fields.** Unknown header keys are ignored, so `defualt:` or `applies-to:` vanish without warning. There is deliberately no `access:`/SD field (the deployed Security Descriptor is live state a shipped file can't know — say access intent in prose if it matters) and no `since:` field (the package version already records provenance).

## Two documenters, one key

If two installed packages document the same `(key, value)`, `regman` does not pick a winner — it shows both records under a `documented by N packages:` banner, flagging the overlap as the anomaly it is. That's a safety net, not a feature: a key should have exactly one documenting package, the one that owns it. If you find yourself documenting another package's keys, that's usually a sign the ownership is wrong.

## Keeping lookups fast

Lookup is correct with no index at all — `regman` scans the corpus directly, and at realistic sizes that's a few milliseconds. An optional index (`regman index`, kept warm by a supervised `regman index --watch`) just skips the scan; it can be absent or stale without ever producing a wrong answer. None of this is your concern as a fragment author, with one exception worth knowing: package installs must **replace** a fragment by atomic rename rather than truncate-and-rewrite, so a lookup never sees a half-written file. peipkg already does this, so simply shipping the file the normal way is correct.

## See also

- [The registry manual](/peios/using-peios/registry-administration/regman.md) — the operator's view of `regman`: reading a knob-card, the `-k` search, and the intent-not-state boundary.
- [Configuration, not storage](/peios/using-peios/registry-concepts/configuration-and-meaning.md) — *why* the registry holds values without their meaning, the idea a `.regman` page exists to serve.
- [Packages](/pekit/recipes/packages.md) and [the recipe format reference](/pekit/reference/recipe-format.md) — installing the fragment as part of your package.

---

# What is the Peios SDK

_Peios / Developing for Peios / SDK basics_

> The Peios SDK is the C-ABI library family you use to talk to Peios from your own programs — access control, the registry, and events. Its first and largest member is libpeios.

The Peios SDK is how your own software talks to Peios.

Peios exposes its security world — identities, tokens, access checks, file security, the registry, and the audit/event stream — through a kernel interface built out of raw syscalls, ioctls, and packed byte buffers in the MS-DTYP wire formats. That interface is precise, but it is not something you want to hand-assemble from C. The SDK is the layer that lifts it into ordinary functions you can call: build a SID, open a token, run an access check, read a registry value, emit an event.

It is a **C ABI**, not a C-only library. The shipping product is a set of hand-written C headers and a shared object with a frozen application binary interface. Anything that can call C — Go via cgo, Rust via `bindgen`, Python via `ctypes`, Zig, C++ — can link it and get the same surface. The library happens to be implemented in Rust, but that is invisible across the boundary: callers see `peios_*` functions, `<peios/*.h>` headers, and errno.

## Who it's for

You want the SDK if you are **building software that runs on Peios and needs to participate in its security model** — a service that checks whether a caller may perform an action, a tool that reads or writes security descriptors, an agent that emits audit events, or anything that reads and writes the registry.

You do *not* need it to simply *run* on Peios. Ordinary POSIX programs run under Peios's Linux-compatibility surface without ever linking libpeios. The SDK is for programs that want to reach past POSIX and speak KACS, LCS, and KMES directly. If you are administering a running system rather than writing code against it, the [Peios operator documentation](/peios/security-fundamentals/identity/overview.md) is the place to start.

## The three surfaces

Peios's kernel boundary has three subsystems, and the SDK mirrors them one-to-one. Everything in the SDK belongs to one of these:

| Subsystem | What it is | SDK headers |
|---|---|---|
| **KACS** — Kernel Access Control Subsystem | Identities, tokens, access decisions, file security, and process security. The heart of the model. | `<peios/security.h>`, `<peios/token.h>`, `<peios/access.h>`, `<peios/file.h>`, `<peios/process.h>` |
| **LCS** — the registry | A hierarchical, transactional, secured key/value store — Peios's system configuration database. | `<peios/registry.h>` |
| **KMES** — the event system | The msgpack-framed audit and event stream: emit events, consume them. | `<peios/msgpack.h>`, `<peios/event.h>` |

The umbrella header `<peios.h>` pulls in all of them; you can also include the individual concept headers for a tighter compile surface.

## libpeios, librsi, and the substrate

The SDK is **two libraries** on a shared foundation, each with its own role:

- **libpeios** — the userspace C-ABI library for KACS, LCS, and KMES. It is the registry *client* and the whole of the access-control and event surface, and it is what the bulk of this documentation describes. Link `-lpeios`, include `<peios.h>`.
- **librsi** — the library for implementing a registry *source* (a storage backend): the provider counterpart to libpeios's registry client. Where libpeios *reads and writes* the registry, librsi is what a program uses to *be* the thing that holds the data and answers the kernel. Link `-lrsi`, include `<rsi.h>`. See [Registry sources](/peios/developing-for-peios/registry-sources/overview.md).
- **peios-cabi** — the internal C-ABI substrate both libraries are built on (the allocator wiring, the errno slot, the syscall/ioctl wrappers, the getxattr-style buffer helpers). You never link it directly; it is shared plumbing, documented here only so the conventions it sets make sense.

So the registry has two sides in this SDK: the **client** side in libpeios (open keys, read/write values) and the **source** side in librsi (back the keys and values, serve the kernel's requests). Most programs are clients; you reach for librsi only when you are providing storage.

Because the two libraries share one substrate, one error model, and one audience, they share one doc set. Learn the conventions once and they hold across both.

## What this documentation promises

This is user-facing documentation, not a specification. The authoritative byte-level contracts live in the PCSA specifications — PCDS for the core data structures, PGSS, PSPK and PSPU for the interfaces — and in the `<pkm/*.h>` kernel headers; where a detail is normative, this documentation points you at the specification that owns it. Where a detail is about *this implementation* rather than the contract, it points at the Peios Kernel manual instead. What you get *here* is the working knowledge to use the library well: what every function does, what it returns, how memory and errors flow, and how the pieces fit together — with enough coverage that you should not need to open the library's source to answer a question. When you find a gap, that is a documentation bug worth reporting.

## Where to go next

- **[Installing and linking](/peios/developing-for-peios/sdk-basics/installing-and-linking.md)** — the packages, the headers, and how to build against the library.
- **[Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md)** — the error model, the memory rules, and the buffer protocol that every function in the SDK follows. Read this once; it saves you re-learning it per module.
- **[Your first program](/peios/developing-for-peios/sdk-basics/your-first-program.md)** — a small, complete program you can compile and run.

---

# Installing and linking

_Peios / Developing for Peios / SDK basics_

> The packages that make up libpeios, the headers you include, and how to compile and link against the library — with pkg-config, by hand, statically, and from non-C languages.

libpeios ships as a small set of packages, split the same way a C library conventionally is: a lean runtime package that programs depend on, and a development package that carries the headers and the linker symlink. You install the runtime everywhere the library is used and the development package only where you compile.

## The packages

| Package | Contents | When you need it |
|---|---|---|
| `libpeios` | The versioned shared object `libpeios.so.0` (the runtime soname). | At **runtime**, on every machine that runs a program linked against the library. Pulled in automatically as a dependency of anything built against it. |
| `libpeios-devel` | The public headers (`peios.h` + `peios/*.h`), the unversioned `libpeios.so` linker symlink, and the `peios.pc` pkg-config descriptor. Depends on a matching `libpeios`. | At **build time**, on machines where you compile. |
| `libpeios-static` | The static archive `libpeios.a`. | Only if you link the library **statically** instead of against the shared object. |
| `libpeios-debuginfo` | Split DWARF debug info, build-id indexed. | Debugging or profiling through the library. |
| `libpeios-debugsource` | The referenced Rust sources for the debug info. | Stepping into the library's own source in a debugger. |

Installing `libpeios-devel` pulls in the matching `libpeios` runtime automatically — the development package pins the exact runtime version whose ABI its headers describe, so the headers you compile against and the shared object you load can never disagree.

## The headers

Everything lives under `<peios/…>`, with one umbrella:

```c
#include <peios.h>          /* the whole API */
```

or, for a tighter compile surface, just the concept headers you use:

```c
#include <peios/security.h> /* SIDs, security descriptors, ACLs */
#include <peios/token.h>    /* tokens */
#include <peios/access.h>   /* access checks */
#include <peios/file.h>     /* file security */
#include <peios/process.h>  /* process security */
#include <peios/registry.h> /* the LCS registry */
#include <peios/msgpack.h>  /* msgpack framing */
#include <peios/event.h>    /* the KMES event stream */
```

The headers are hand-written and are the real API — they carry the prose docs and the layout notes that a generated header can't. (Internally they are checked against the library's Rust surface by a cbindgen-based verifier, so they cannot silently drift from what the shared object actually exports. You do not interact with that machinery; it just means the header you read is the contract you get.)

### The `<pkm/*.h>` dependency

The Peios headers do not re-invent the kernel's wire constants — they use them directly. So `<peios/security.h>` includes `<pkm/sid.h>` and `<pkm/sd.h>`, and the other headers pull in their matching `<pkm/*.h>` UAPI headers for the `KACS_*`, `LCS_*`, and `KMES_*` constants and the `#[repr(C)]` argument structs. Those PKM kernel UAPI headers must be on your include path when you compile. They ship with the Peios kernel headers; on a normal Peios development install they are already where the toolchain looks. If a build fails with `fatal error: pkm/sid.h: No such file or directory`, that is what is missing — add the kernel UAPI include directory to your compiler's search path.

## Compiling and linking

### With pkg-config (recommended)

The development package installs a `peios.pc` descriptor, so pkg-config knows the include and library flags:

```sh
cc myprog.c $(pkg-config --cflags --libs peios) -o myprog
```

`pkg-config --cflags peios` expands to the include flags and `--libs peios` to `-lpeios` plus the library directory. This is the form to prefer — it stays correct across install prefixes and multiarch library directories.

### By hand

If you are not using pkg-config, link against `-lpeios` directly:

```sh
cc myprog.c -lpeios -o myprog
```

Add `-I` / `-L` flags if your headers and library live outside the compiler's default search paths.

### Statically

Install `libpeios-static` and point the linker at the archive:

```sh
cc myprog.c -o myprog /usr/lib/x86_64-linux-peios/libpeios.a
```

The static archive is also what the library's own integration tests link against, so it is a fully supported way to build. Static linking folds the library into your binary — you then do not need the `libpeios` runtime package on the target, though you still need whatever C runtime your program uses.

## Linking from other languages

Because libpeios is a C ABI, any language with a C FFI can call it. The shape is always the same: point the FFI at the `libpeios.so.0` shared object (or the static archive), declare the `peios_*` functions with their C signatures, and follow the [library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md) for buffers and errors exactly as a C caller would.

- **Rust** — use the maintained bindings: the safe **`peios`** crate (or the raw `peios-sys`), covered in [Using the SDK from Rust](/peios/developing-for-peios/sdk-basics/using-the-sdk-from-rust.md). You don't need to hand-roll `bindgen`. (The library *is* Rust internally, but the crates still bind it strictly as a C library through the stable ABI — the supported entry point.)
- **Go** — cgo against `<peios.h>` with `#cgo pkg-config: peios`.
- **Python** — `ctypes` or `cffi` against `libpeios.so.0`.
- **C++** — include the headers directly; they are wrapped in `extern "C"` and compile as C++.

The one rule that matters across every language: the errno-based error model and the caller-buffer / two-call buffer protocol are part of the contract, not a C convenience. Read [Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md) before you wrap the API in another language's idioms.

## librsi — the registry-source library

Everything above describes **libpeios**. If you are writing a [registry source](/peios/developing-for-peios/registry-sources/overview.md), you link the sibling library **librsi** instead (or as well). It is built on the same [`peios-cabi` substrate](/peios/developing-for-peios/sdk-basics/what-is-the-peios-sdk.md), follows the identical [library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md) — raw fds, the `int`/`ssize_t` error model, the borrow discipline — and is packaged exactly the same way libpeios is.

### The packages

| Package | Contents | When you need it |
|---|---|---|
| `librsi` | The versioned shared object `librsi.so.0`. | At **runtime**, wherever a source runs. |
| `librsi-devel` | The public headers (`rsi.h` + `rsi/*.h`), the `librsi.so` linker symlink, and the `rsi.pc` pkg-config descriptor. Depends on a matching `librsi`. | At **build time**. |
| `librsi-static` | The static archive `librsi.a`. | Only for static linking. |
| `librsi-debuginfo` | Split DWARF debug info. | Debugging or profiling through the library. |
| `librsi-debugsource` | The referenced Rust sources. | Stepping into the library's own source. |

As with libpeios, `librsi-devel` pulls in the matching `librsi` runtime and the `kernel-headers` package — the `<rsi/*.h>` headers include `<pkm/lcs.h>`, so the same UAPI-header requirement described above applies.

### Headers and linking

Include the umbrella or the individual concept headers:

```c
#include <rsi.h>            /* the whole librsi API */
/* or: */
#include <rsi/source.h>    /* registration */
#include <rsi/request.h>   /* decoding requests */
#include <rsi/response.h>  /* building responses */
```

and compile with pkg-config (the descriptor's module name is `rsi`):

```sh
cc mysource.c $(pkg-config --cflags --libs rsi) -o mysource
```

or link `-lrsi` directly, or against `librsi.a` for a static build — exactly the three forms shown for libpeios above. The two libraries are independent: a program can be a client (libpeios), a source (librsi), or both, linking each as needed.

---

# Your first program

_Peios / Developing for Peios / SDK basics_

> A small, complete C program that parses a security descriptor, reads its owner, and formats the SID back to text — putting the two-call protocol, views, and the error model to work.

This is a complete program you can compile and run. It does not touch the kernel — it works entirely with the security-descriptor *vocabulary* from `<peios/security.h>`, which makes it a safe first outing: no privileges, no live tokens, just the conventions from [Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md) put to work.

The task: take a security descriptor written in SDDL text, turn it into wire bytes, read its owner, and print that owner's SID in string form. Along the way you exercise the two-call buffer protocol twice, parse with a zero-copy view, and handle errors the libpeios way.

## The program

```c
#include <peios/security.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    /* An SDDL descriptor: owner and group are Administrators (BA), with a
       DACL granting them full access (FA). */
    const char *sddl = "O:BAG:BAD:(A;;FA;;;BA)";

    /* 1. Parse the SDDL text into self-relative SD wire bytes.
          First call probes for the size (cap == 0), then we retrieve. */
    ssize_t need = peios_sddl_parse_sd(NULL, 0, sddl);
    if (need < 0) {
        fprintf(stderr, "parse probe failed: %s\n", strerror(errno));
        return 1;
    }

    unsigned char *sd = malloc((size_t)need);
    if (!sd)
        return 1;

    ssize_t sd_len = peios_sddl_parse_sd(sd, (size_t)need, sddl);
    if (sd_len < 0) {
        fprintf(stderr, "parse failed: %s\n", strerror(errno));
        free(sd);
        return 1;
    }
    printf("security descriptor: %zd bytes\n", sd_len);

    /* 2. Parse the SD into a zero-copy view. The view borrows `sd` — it
          must stay alive and unmodified until we are done reading. */
    peios_sd_view view;
    if (peios_sd_parse(sd, (size_t)sd_len, &view) != 0) {
        fprintf(stderr, "sd parse failed: %s\n", strerror(errno));
        free(sd);
        return 1;
    }

    /* 3. Pull out the owner SID. On success `owner` points INTO `sd`. */
    const void *owner;
    size_t owner_len;
    if (peios_sd_view_owner(&view, &owner, &owner_len) != 0) {
        printf("no owner set\n");
        free(sd);
        return 0;
    }

    /* 4. Format the owner SID as its "S-1-…" string. The byte form of a
          SID is bounded, but its string form is variable, so we probe.
          A string length excludes the NUL, so allocate len + 1. */
    ssize_t slen = peios_sid_format(owner, owner_len, NULL, 0);
    if (slen < 0) {
        fprintf(stderr, "sid format probe failed: %s\n", strerror(errno));
        free(sd);
        return 1;
    }

    char *str = malloc((size_t)slen + 1);
    if (!str) {
        free(sd);
        return 1;
    }
    if (peios_sid_format(owner, owner_len, str, (size_t)slen + 1) < 0) {
        fprintf(stderr, "sid format failed: %s\n", strerror(errno));
        free(str);
        free(sd);
        return 1;
    }

    printf("owner: %s\n", str);

    /* 5. Only now free `sd` — every pointer the view gave us pointed into
          it, so it had to outlive the last read. */
    free(str);
    free(sd);
    return 0;
}
```

## Building and running

```sh
cc first.c $(pkg-config --cflags --libs peios) -o first
./first
```

Expected output:

```
security descriptor: 44 bytes
owner: S-1-5-32-544
```

`S-1-5-32-544` is the well-known SID for the local **Administrators** group — which is exactly the `BA` you wrote in the SDDL string. (The byte count may differ across versions; the owner will not.)

## What just happened

Every convention from the previous page showed up here:

- **The two-call protocol, twice.** `peios_sddl_parse_sd` and `peios_sid_format` were each called first with a `NULL`/`0` buffer to learn the size, then again to fill a right-sized allocation. Neither could ever truncate: a too-small buffer would have returned `-1` with `ERANGE`, not a partial result.
- **A string length excludes the NUL.** That is why the SID string buffer was `slen + 1`.
- **A zero-copy view borrowed our buffer.** `owner` pointed *into* `sd`, so `sd` had to stay alive and unmodified until after the final `peios_sid_format`. Freeing it earlier would have left `owner` dangling — the one mistake this pattern invites, and the reason step 5 frees last.
- **Errors came back through the return value and `errno`.** Every call was checked; `strerror(errno)` explained any failure. No exceptions, no out-of-band error channel.

## A shortcut worth knowing

Not every buffer needs a probe. Some results have a known ceiling, and the library gives you a constant so you can use a fixed stack buffer and skip the first call entirely. A SID is the classic case — it is never larger than `PEIOS_SID_MAX_BYTES`:

```c
unsigned char sid[PEIOS_SID_MAX_BYTES];
ssize_t n = peios_sid_well_known(sid, sizeof sid, PEIOS_WKS_ADMINISTRATORS);
/* n > 0: `sid` holds S-1-5-32-544 in wire form, no malloc, no probe. */
```

Use the probe when a length is genuinely unbounded (strings, whole descriptors, ACLs); use the fixed-size shortcut when the module documents a ceiling for the thing you are building.

## Where to go next

You now have the mechanics. From here, pick the subsystem you need:

- **[Access control (KACS)](/peios/developing-for-peios/sdk-access-control/overview.md)** — the security vocabulary you just used, plus tokens, access checks, file security, and process security.
- **[The registry (LCS)](/peios/developing-for-peios/sdk-registry/overview.md)** — reading and writing Peios's configuration store.
- **[Events (KMES)](/peios/developing-for-peios/sdk-events/overview.md)** — emitting and consuming the audit/event stream.

---

# Using the SDK from Rust

_Peios / Developing for Peios / SDK basics_

> The Peios SDK ships first-class Rust bindings — the peios-sys raw FFI crate and the safe, idiomatic peios wrapper. This page is the on-ramp; the crate's rustdoc is the API reference.

The SDK is a C ABI, so it is reachable from any language with a C FFI — but Rust gets **first-class, maintained bindings** rather than hand-rolled `extern` blocks. If you are writing Rust on Peios, use them. This page is the on-ramp: what the crates are, how to wire them into a build, and where the docs live. It is deliberately *not* an API reference — that job belongs to the crate's rustdoc, which is generated from the code and so never drifts from it.

## The two crates

`peios-rs` is two crates, layered:

| Crate | What it is | Use it when |
|---|---|---|
| **`peios-sys`** | Raw, unsafe FFI. Bindings are generated at build time by `bindgen` from the hand-written `<peios.h>` (the shipping API, which the ABI verifier proves is identical to the Rust source). | You need the raw C surface — an escape hatch, or to build your own abstractions. |
| **`peios`** | The safe, idiomatic wrapper: RAII handle types, `Result`-returning methods, typed wire-constant families, and owned buffers — so you never touch a raw fd, a sticky-error builder, or a getxattr-style size probe directly. | Almost always. This is the crate you want. |

Both bind libpeios strictly as a **C library** through its stable ABI — they never depend on libpeios's internal Rust crates. The C ABI is the only supported entry point, and binding through it means the crates exercise the exact surface every other consumer uses.

The safe crate's modules mirror the libpeios concept headers one-for-one — `security`, `token`, `access`, `file`, `process`, `event`, `msgpack`, `registry` — so everything you learn about the C surface maps straight across.

## Where the docs live

Two places, and the split is deliberate:

- **The API reference is the crate's rustdoc.** Every public item in `peios` is documented (the crate is built with `#![warn(missing_docs)]`, so this is enforced), with a crate-level overview, per-module docs, and the error model. Build and browse it locally with:

  ```sh
  cargo doc -p peios --open
  ```

  This is the reference precisely *because* it is generated from the code — it can never fall out of sync with the actual method signatures and types the way hand-written prose would.

- **The concepts live in these docs.** The *what* and *why* — what a token is, how the [two-call protocol](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#the-two-call-buffer-protocol) works, how an [access check](/peios/developing-for-peios/sdk-access-control/checking-access.md) reaches a verdict, the [RSI source model](/peios/developing-for-peios/registry-sources/overview.md) — are identical whether you call from C or Rust, and they are documented once, here. The safe crate mirrors the concept headers, so each learn section maps to a module:

  | Rust module | Concepts in |
  |---|---|
  | `security`, `token`, `access`, `file`, `process` | [Access control](/peios/developing-for-peios/sdk-access-control/overview.md) |
  | `registry` | [The registry](/peios/developing-for-peios/sdk-registry/overview.md) |
  | `event`, `msgpack` | [Events](/peios/developing-for-peios/sdk-events/overview.md) |

  Read the concept page for the model, then the rustdoc for the exact Rust API.

## Adding the dependency

Add the `peios` crate to your `Cargo.toml`:

```toml
[dependencies]
peios = { git = "https://github.com/peios/peios-rs" }
```

(or a path/registry dependency, however you consume Peios crates). The features:

| Feature | Effect |
|---|---|
| *default* | Dynamic linking against `libpeios.so` — the intended production model (one soname-versioned system copy; fixes ship once). |
| `static` | Static linking against `libpeios.a`. See the [caveat below](#linking-dynamic-vs-static). |
| `uapi` | Reuse the canonical Rust mirror of the `pkm` UAPI types (`peios-uapi`) instead of letting `bindgen` emit its own copy, so the `kacs_*`/`pkm` types are one identity across crates. |

## A first taste

The safe crate turns the C conventions into ordinary Rust. Compare this with the C [first program](/peios/developing-for-peios/sdk-basics/your-first-program.md) — no size probes, no manual `close`, errors as `Result`:

```rust
use peios::token::{Token, TokenAccess};

fn main() -> peios::Result<()> {
    // Open my own token; the handle closes on drop.
    let me = Token::open_self(false, TokenAccess::QUERY)?;

    let sid = me.user()?;          // owned Sid, not a caller buffer + length
    let il  = me.integrity()?;     // typed IntegrityLevel, not a raw u32
    println!("running as {sid} at integrity {il:?}");

    Ok(())
}
```

The idiomatic wins show up most in the fiddly places. Impersonation, for instance, is an RAII guard — identity reverts when the guard drops, so it is exception-safe by construction rather than needing a manual `revert` in every cleanup path:

```rust
use std::os::fd::BorrowedFd;

fn serve(conn: BorrowedFd<'_>) -> peios::Result<()> {
    let caller = Token::open_peer(conn)?;
    let _guard = caller.impersonate_scoped()?;   // now acting as the caller
    do_work_as_caller()?;                        // any early return still reverts
    Ok(())
    // `_guard` drops here (or on the `?` above) → identity reverts automatically
}
```

Success-side status values — a file's opened-vs-created disposition, a key's created-new-vs-opened — come back *alongside* the handle, not as errors; genuine failures are the `Err(Error)` side, where `Error` wraps the `errno` the C ABI set.

## Linking: dynamic vs static

This is the one Rust-specific wrinkle worth understanding, because the static path has a sharp edge.

- **Dynamic (default)** links `libpeios.so` — the production model, and the path a normal `std` Rust program must use.
- **Static (`--features static`)** links `libpeios.a`. That archive is a Rust *staticlib*: it bakes in its own copy of the Rust runtime (the `panic = "abort"` handler, the global-allocator shim, the alloc-error handler). Linking it into a consumer that *also* carries that runtime — i.e. **any Rust binary that links `std`** — collides on those symbols (`rust_begin_unwind`, `__rust_alloc_error_handler`, …). So the static path is for **C consumers** and `no_std` Rust binaries that supply no conflicting runtime. A `std` Rust consumer — including `cargo test` — **must use the dynamic path.**

Keep that rule in mind and static linking is a non-issue: reach for it only from `no_std`, and use the default dynamic link everywhere else.

## Finding libpeios at build time

`peios-sys`'s build script resolves the library and headers in this order:

1. **Environment override** — set all three:
   - `PEIOS_LIB_DIR` — the directory containing `libpeios.{so,a}`
   - `PEIOS_INCLUDE` — the directory containing `<peios.h>`
   - `PKM_UAPI` — the directory containing `<pkm/*.h>` (referenced by `peios.h`'s signatures)
2. **pkg-config** — otherwise, if libpeios installs its `peios.pc` on `PKG_CONFIG_PATH`.

On a system where libpeios is installed from its [packages](/peios/developing-for-peios/sdk-basics/installing-and-linking.md#the-packages), pkg-config just works and you set nothing. For a **local checkout**, point the three variables at your build tree:

```sh
PEIOS_LIB_DIR=../libpeios/target/release \
PEIOS_INCLUDE=../libpeios/include \
PKM_UAPI=../pkm/uapi \
cargo build
```

One bring-up gotcha for local dynamic builds: the crate bakes an rpath that resolves libpeios by its **soname** (`libpeios.so.0`), but a plain `cargo build` of libpeios emits only the unversioned `libpeios.so`. Create the soname link once so the run resolves:

```sh
ln -sf libpeios.so ../libpeios/target/release/libpeios.so.0
```

(This is a checkout convenience — on a real system the packaged `libpeios.so.0` is already there.)

## The bottom line

- Depend on the **`peios`** crate; drop to `peios-sys` only for the raw surface.
- **Concepts:** these docs. **API reference:** `cargo doc -p peios --open`.
- Use the **default dynamic** link unless you are `no_std`.

Everything the [library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md) page teaches still holds underneath — the crate just wraps it in Rust idiom.

---

# Access control overview

_Peios / Developing for Peios / Access control_

> How the KACS pieces fit together from a developer's seat — identities, tokens, security descriptors, and access checks — and which part of the SDK you reach for.

KACS — the Kernel Access Control Subsystem — is the heart of Peios's security model, and it is the part of the SDK you are most likely to reach for. This section is a tour: it explains how the pieces fit together and points you at the right guide (and the right reference page) for each task. If you have not yet read the [library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md), read those first — everything here assumes the error and buffer rules they describe.

## The four nouns

Almost everything in KACS is built from four things:

| Noun | What it is | SDK home |
|---|---|---|
| **SID** | The unique binary name of a principal — a user, group, machine, or well-known system actor. | [`security.h`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#sids) |
| **Security descriptor (SD)** | What protects an object: its owner, its group, and the ACLs that grant or deny access. Built from ACEs, each naming a SID and an access mask. | [`security.h`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#building-security-descriptors) |
| **Token** | The runtime object that carries an identity — a user SID, groups, privileges, an integrity level, claims. Every access decision is made *against a token*. A token is a file descriptor. | [`token.h`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md) |
| **Access check** | The act of deciding whether a token may perform a desired access on an object, given the object's SD. | [`access.h`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md) |

The relationship is simple to state: an **access check** asks whether a **token** (the subject) is granted some access to an object protected by a **security descriptor**, and both the token and the SD are expressed in terms of **SIDs**.

## The shape of a decision

When you need to make an authorisation decision in your own code, the pattern is almost always the same three steps:

1. **Get the subject's token.** Usually the caller's — often via [`peios_token_open_peer`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md#opening-and-creating-tokens) on a socket, so you learn who connected — or your own effective token (`token_fd = -1`).
2. **Get the object's security descriptor.** You either hold it already, read it off a file with [`peios_file_get_sd`](/peios/developing-for-peios/sdk-reference/sdk-files/file-h-file-security.md), or build one with a [`peios_sd_builder`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#building-security-descriptors).
3. **Run the check.** [`peios_access_check`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md#the-check) tells you whether the desired rights are granted, and exactly which subset was granted.

The [checking access](/peios/developing-for-peios/sdk-access-control/checking-access.md) guide walks that end to end.

## Advisory versus enforced

There is one distinction worth internalising early. The SDK's access check is **advisory**: it computes what the answer *would* be. It does not *enforce* anything — enforcement of a real operation (opening a file, adjusting a token) happens inside the kernel against the subject's own process security block.

So you use `peios_access_check` when **your** code is the resource manager: you own some object — a record in your database, a slot in your service — that isn't a kernel object, and you want to make the grant/deny decision using Peios identities and the same rules the kernel would apply. For actual kernel objects (files, tokens, registry keys), you don't pre-check and then act; you just act, and the kernel enforces, returning `EACCES` if denied.

## Identity is not always *your* identity

A recurring theme in KACS is acting as someone other than yourself:

- **Impersonation** lets a service temporarily adopt a caller's identity so its access checks run as *them* — the way a server does work "on behalf of" a client without running as root and hand-rolling permission logic. See [working with tokens](/peios/developing-for-peios/sdk-access-control/working-with-tokens.md).
- **Restricted tokens** let you *drop* power — derive a strictly less-privileged token to hand to less-trusted code.
- **Integrity levels** and **confinement** bound what a token can touch regardless of its SIDs.

These are what make KACS more than POSIX uid/gid, and the token module is where you reach for them.

## Where to go in this section

- **[Working with tokens](/peios/developing-for-peios/sdk-access-control/working-with-tokens.md)** — who am I, who is calling me, and how to act as someone else.
- **[Checking access](/peios/developing-for-peios/sdk-access-control/checking-access.md)** — building a security descriptor and making a decision end to end.
- **[Securing files](/peios/developing-for-peios/sdk-access-control/securing-files.md)** — the native open and reading/writing file security descriptors.
- **[Hardening a process](/peios/developing-for-peios/sdk-access-control/hardening-a-process.md)** — turning on process mitigations.

For the exhaustive per-function detail behind any of these, the [reference section](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md) documents every symbol.

---

# Working with tokens

_Peios / Developing for Peios / Access control_

> Answer "who am I?", "who is calling me?", and "act as someone else" with the KACS token API — opening tokens, peer identity over sockets, impersonation, and dropping privilege.

A **token** is the runtime carrier of an identity, and it is a file descriptor. This guide covers the everyday token tasks; [`token.h`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md) is the exhaustive reference for every call and field.

## Who am I?

To inspect your own identity, open your effective token and query it:

```c
int tok = peios_token_open_self(0, KACS_TOKEN_QUERY);
if (tok < 0) { /* errno */ }

unsigned char sid[PEIOS_SID_MAX_BYTES];
ssize_t n = peios_token_user(tok, sid, sizeof sid);   /* the user SID */

uint32_t il;
peios_token_integrity(tok, &il);                      /* integrity level RID */

struct peios_privilege_set privs;
peios_token_privileges(tok, &privs);                  /* held/enabled privileges */

close(tok);
```

`peios_token_open_self` gives you the *effective* token — if your thread is impersonating, that's the impersonated identity. Pass `KACS_TOKEN_OPEN_REAL` in the flags to get your process's real primary token regardless. The `access` argument is the handle rights you want; `KACS_TOKEN_QUERY` is enough to read.

Group SIDs and other list-valued classes come back as buffers you parse with the [`security.h` views](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#sid-and-attributes-arrays): read `CLASS_GROUPS` with [`peios_token_query`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md#query), then `peios_sid_array_parse` it.

## Who is calling me?

The most useful token trick in a service is learning the identity of whoever connected to your socket. When a client connects over a Unix stream or seqpacket socket, KACS captures their token at `connect()` time, and you open it from the accepted connection:

```c
int conn = accept(listener, NULL, NULL);
int caller = peios_token_open_peer(conn);   /* QUERY | IMPERSONATE rights */
if (caller < 0) { /* errno */ }

/* Now query the caller's identity, or impersonate them (below). */
```

This is local authentication with no passwords and no handshake — the kernel vouches for who is on the other end. The handle comes with fixed `QUERY | IMPERSONATE` rights, which is exactly what a server needs.

## Acting as the caller

Once you hold a caller's (impersonation) token, you can **impersonate** them: adopt their identity on your current thread so every subsequent access check runs as *them*, not as your service. This is how you do work on a client's behalf without running privileged and re-implementing their permissions.

```c
if (peios_token_impersonate(caller) != 0) { /* errno */ }

/* ... do the work here: file opens, access checks, etc. all run as the caller ... */

peios_token_revert();   /* back to your own identity */
close(caller);
```

Always pair `peios_token_impersonate` with [`peios_token_revert`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md#impersonation-and-installation), ideally in the cleanup path, so a failure partway through can't leave your thread wearing someone else's identity. `peios_token_revert` is a safe no-op if you weren't impersonating.

The full flow for a request handler is: `accept` → `peios_token_open_peer` → `peios_token_impersonate` → serve the request → `peios_token_revert` → `close`.

## Dropping power

To run less-trusted code with less authority than you hold, derive a **restricted** token and hand it over. `peios_token_restrict` can delete privileges, demote groups to deny-only, and add restricting SIDs:

```c
struct peios_token_restrict spec = {
    .privs_to_delete = KACS_SE_DEBUG_PRIVILEGE | KACS_SE_IMPERSONATE_PRIVILEGE,
    .flags           = KACS_TOKEN_RESTRICT_WRITE_RESTRICTED,
};
int weak = peios_token_restrict(my_primary, &spec);
```

The result is a strictly less-powerful token. Combined with [integrity levels](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#integrity-levels) and confinement (both set when [minting a token](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md#the-token-spec-builder)), this is the basis of sandboxing on Peios.

## Minting tokens

Creating a token from scratch requires `SeCreateTokenPrivilege` and is the province of authentication authorities, not ordinary programs. When you do need it, the [token-spec builder](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md#the-token-spec-builder) is the ergonomic path — typed setters for the user SID, groups, privileges, integrity, claims, and the rest, then `peios_token_builder_create`. Mind the [index convention](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md#the-index-convention) for owner/primary-group references, and don't add the logon SID yourself — the kernel injects it.

## Next

- **[Checking access](/peios/developing-for-peios/sdk-access-control/checking-access.md)** — use a token to make an authorisation decision.
- **[`token.h` reference](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md)** — every token call in full.
- **[Impersonation](/peios/security-fundamentals/impersonation/overview.md)** — the operator-side model and its two gates.

---

# Checking access

_Peios / Developing for Peios / Access control_

> Make an authorisation decision end to end — build a security descriptor, run an access check against a token, and interpret the granted mask.

This guide walks a complete access decision: you have an object to protect, a caller to check, and you want KACS to tell you what they're allowed. The exhaustive detail lives in [`access.h`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md) and [`security.h`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md); here we put them together.

## When to use this

Reach for [`peios_access_check`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md#the-check) when **your** program is the resource manager — you own something that isn't a kernel object (a document, an API route, a record) and you want to gate it with Peios identities and rules. For actual kernel objects, don't pre-check; just perform the operation and let the kernel enforce.

Remember the check is **advisory**: it computes the answer, you enforce it.

## Step 1 — describe what protects the object

An object is protected by a security descriptor. If you don't already have one, build it. Say the resource should be readable and writable by its owner and read-only for a "viewers" group:

```c
/* An ACL: allow OWNER full, allow VIEWERS read. */
peios_acl_builder *acl = peios_acl_builder_new();
peios_acl_builder_allow(acl, owner_sid, owner_len, KACS_ACCESS_ALL, 0);
peios_acl_builder_allow(acl, viewers_sid, viewers_len, KACS_ACCESS_READ, 0);

size_t acl_len;
const void *acl_bytes = peios_acl_builder_bytes(acl, &acl_len);

/* Wrap it in a security descriptor with an owner. */
peios_sd_builder *sd = peios_sd_builder_new();
peios_sd_builder_owner(sd, owner_sid, owner_len);
peios_sd_builder_dacl(sd, acl_bytes, acl_len);

size_t sd_len;
const void *sd_bytes = peios_sd_builder_bytes(sd, &sd_len);
```

(You can also write the descriptor as [SDDL text](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#sddl-text-codec) and parse it — often easier when the policy is fixed.)

## Step 2 — identify the subject

The subject is a token. Most often it's the caller you [opened from a socket](/peios/developing-for-peios/sdk-access-control/working-with-tokens.md#who-is-calling-me), or your own effective token. In the request you pass either a token fd or `-1` for your own effective token.

## Step 3 — run the check

Fill in a request and call. `desired` is what you're testing; `mapping` folds any generic rights to the object class's specific bits (use the class's published mapping — here we'll treat the object like a file):

```c
struct peios_access_request req = {
    .token_fd = caller_fd,               /* or -1 for my own token */
    .sd       = sd_bytes, .sd_len = sd_len,
    .desired  = KACS_ACCESS_READ | KACS_ACCESS_WRITE,
    .mapping  = peios_file_generic_mapping,
};

uint32_t granted = 0;
int rc = peios_access_check(&req, &granted, NULL);
```

## Step 4 — interpret the result

```c
if (rc == 0) {
    /* Every desired right was granted. */
} else if (errno == EACCES) {
    /* Denied. `granted` still holds what WAS allowed — e.g. maybe READ
       succeeded but WRITE did not. Decide per-right from `granted`. */
    bool may_read  = granted & KACS_ACCESS_READ;
    bool may_write = granted & KACS_ACCESS_WRITE;
} else {
    /* A real error: bad token fd, malformed SD, etc. */
    perror("access_check");
}
```

The key idea: a denial is **not** an error to log and bail on — it's the expected "no". And because `granted` is filled even on denial, you can ask for a broad set of rights in one call and read back exactly which subset the subject has, rather than probing right by right. Clean up the builders (`peios_acl_builder_free`, `peios_sd_builder_free`) and the token fd when done.

## Per-property checks

If your object has properties or property-sets with their own object ACEs, evaluate the whole tree in one call with [`peios_access_check_list`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md#the-object-type-list-variant): supply an object-type tree and get one result per node, so you learn (for instance) that a caller may read most of an object but not one protected field — without a separate check per field.

## Auditing a decision

Pass a non-`NULL` [`peios_access_audit`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md#audit-outputs) to learn what a `SYSTEM_AUDIT` ACE match would log, and whether a *staged* central access policy would decide differently — the signal you watch when rolling out a policy change.

## Next

- **[`access.h` reference](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md)** — every field of the request and the audit outputs.
- **[Access decisions](/peios/security-fundamentals/access-decisions/overview.md)** — the operator-side account of how KACS reaches a verdict (and how to debug a surprising denial).

---

# Securing files

_Peios / Developing for Peios / Access control_

> Open files the native KACS way, and read and write a file's security descriptor by path or by fd.

Peios files carry real security descriptors, and the SDK opens them with a native KACS open rather than POSIX `open()`. This guide covers the two everyday tasks: opening a file with a specific access, and reading or changing a file's security. The full surface is in [`file.h`](/peios/developing-for-peios/sdk-reference/sdk-files/file-h-file-security.md).

To keep the fragments readable, most error checks are elided here — every call below returns `-1` with `errno` on failure, and real code must check each one (see [Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md)).

## The native open

[`peios_file_open`](/peios/developing-for-peios/sdk-reference/sdk-files/file-h-file-security.md#opening-a-file) is shaped like `NtCreateFile`: you state the access you want, what to do about existence (the *disposition*), any create options, and — when creating — the security descriptor to stamp on the new file. It returns an ordinary Linux fd whose **granted access is fixed for the fd's lifetime**, which means you can safely hand it to another process by `SCM_RIGHTS`, `dup`, or across `exec`: the fd carries exactly the access it was opened with.

Open-or-create a file, readable and writable, stamping a creator SD if it's new:

```c
struct peios_open_params p = {
    .desired_access = KACS_FILE_READ_DATA | KACS_FILE_WRITE_DATA,
    .disposition    = KACS_DISPOSITION_OPEN_IF,   /* open existing, else create */
    .sd             = creator_sd, .sd_len = creator_sd_len,   /* used only on create */
};

uint32_t status = 0;
int fd = peios_file_open(AT_FDCWD, "state.db", &p, &status);
if (fd < 0) { perror("open"); return -1; }

if (status == KACS_STATUS_CREATED) { /* we made it */ }
else                               { /* it already existed */ }
```

`status_out` tells you *what happened* — created versus opened versus overwritten — without a separate `stat` and its attendant race. If you're only ever opening existing files, use a plain open disposition and pass `sd = NULL`.

## Reading a file's security descriptor

To see who can do what to a file, read its SD. `secinfo` selects which components you want — owner, group, DACL, SACL — so you fetch only what you need:

```c
/* Probe, allocate, read (two-call). */
ssize_t need = peios_file_get_sd(AT_FDCWD, "state.db",
                                 KACS_SECINFO_OWNER | KACS_SECINFO_DACL,
                                 NULL, 0, 0);
void *sd = malloc(need);
peios_file_get_sd(AT_FDCWD, "state.db",
                  KACS_SECINFO_OWNER | KACS_SECINFO_DACL, sd, need, 0);

/* Parse it with a security.h view. */
peios_sd_view v;
peios_sd_parse(sd, need, &v);
peios_acl_view dacl;
if (peios_sd_view_dacl(&v, &dacl) == 0) {
    unsigned n = peios_acl_view_count(&dacl);
    /* iterate ACEs … */
}
free(sd);
```

If you already hold a file fd, use the fd-targeted [`peios_fd_get_sd`](/peios/developing-for-peios/sdk-reference/sdk-files/file-h-file-security.md#by-fd) instead of a path — no second path resolution, and for a normal file fd the check uses the access already baked in at open.

## Changing a file's security descriptor

Writing an SD is component-selective too: name the components you're changing in `secinfo`, and everything you *don't* name is preserved. To tighten a file's DACL without touching its owner or SACL:

```c
/* Build an SD carrying only a DACL. */
peios_sd_builder *b = peios_sd_builder_new();
peios_sd_builder_dacl(b, new_acl, new_acl_len);
size_t sd_len; const void *sd_bytes = peios_sd_builder_bytes(b, &sd_len);

peios_file_set_sd(AT_FDCWD, "state.db", KACS_SECINFO_DACL, sd_bytes, sd_len, 0);
peios_sd_builder_free(b);
```

Because only `KACS_SECINFO_DACL` is selected, the owner, group, and SACL are left exactly as they were. The [`security.h` builders](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#building-security-descriptors) are how you assemble the SD to apply.

## Pre-flighting an open

Sometimes you want to know whether a caller *could* open a file before you actually do. Read the file's SD, then run an [access check](/peios/developing-for-peios/sdk-access-control/checking-access.md) against the caller's token with `peios_file_generic_mapping` — no open, no side effects, just the verdict.

## Next

- **[`file.h` reference](/peios/developing-for-peios/sdk-reference/sdk-files/file-h-file-security.md)** — every parameter, plus the fd-targeted calls and mount policy.
- **[File access](/peios/security-fundamentals/file-access/overview.md)** — the operator-side model of native file security.

---

# Hardening a process

_Peios / Developing for Peios / Access control_

> Turn on process mitigations to harden your program, and understand the one-way, fail-closed semantics.

Peios lets a process opt into hardening — exploit mitigations enforced by the kernel on that process's security block. The SDK exposes this through a single call, [`peios_process_set_mitigations`](/peios/developing-for-peios/sdk-reference/sdk-processes/process-h.md#setting-mitigations). This short guide covers using it well; the full flag set and semantics are in [`process.h`](/peios/developing-for-peios/sdk-reference/sdk-processes/process-h.md) and the [operator docs](/peios/security-fundamentals/process-mitigations/overview.md).

## Harden yourself at startup

The common case is a program hardening itself early in `main`, before it processes any untrusted input:

```c
#include <peios/process.h>

int main(void)
{
    /* Enforce W^X and shadow stacks; abort if either can't be turned on. */
    if (peios_process_set_mitigations(-1, KACS_MIT_WXP | KACS_MIT_SML) != 0) {
        perror("set_mitigations");
        return 1;   /* refuse to run unhardened */
    }
    /* ... the rest of the program runs with those protections on ... */
}
```

`pidfd == -1` targets the calling process. `mitigations` is a mask of `KACS_MIT_*` bits (from `<pkm/psb.h>`); combine the ones you want and set them together.

## Three things to know

**It's one-way.** Mitigation bits can only be *set*, never cleared — once on, they stay on for the life of the process. That's the point: a mitigation you could turn off is one an attacker could turn off. Treat each call as a permanent commitment.

**It's all-or-nothing, and fails closed.** If any requested protection can't actually be activated, the call changes *nothing* and returns `-1`. You never end up believing a mitigation is on when it isn't. So request the set you require together and check the result once — success means the whole set is active. (Bits from earlier successful calls stay on regardless.)

**Targeting another process is privileged.** To harden a process other than your own, you need `PROCESS_SET_INFORMATION` on it *and* PIP dominance over it — you can't reach into a process you don't already dominate. For self-hardening (`-1`), neither is needed.

## Choosing what to enable

The bit catalogue and what each mitigation defends against is documented operator-side under [process mitigations](/peios/security-fundamentals/process-mitigations/overview.md) (and in the Peios Kernel TRM §3.3, the Process Security Block). A couple of notes for the SDK caller:

- `KACS_MIT_ALL` is the mask of all valid bits — useful for validating input, not usually what you'd blanket-enable without thought.
- `KACS_MIT_CFI` is a legacy alias that expands to `KACS_MIT_CFIF | KACS_MIT_CFIB`.

Enable the specific protections your program can tolerate, verify the call succeeded, and prefer to do it before you touch untrusted data.

## Next

- **[`process.h` reference](/peios/developing-for-peios/sdk-reference/sdk-processes/process-h.md)** — the call in full.
- **[Process mitigations](/peios/security-fundamentals/process-mitigations/overview.md)** — every mitigation and its threat model.

---

# Registry overview

_Peios / Developing for Peios / The registry_

> How LCS — the layered registry — works from a developer's seat: keys, values, layers and precedence, transactions, and watches.

LCS — the Layered Configuration Subsystem — is Peios's registry: a kernel-mediated, hierarchical, secured configuration store. If you have used the Windows registry it will feel familiar — a tree of **keys** holding typed **values** — but LCS adds one defining idea: **layers**. This section teaches the client API; [`registry.h`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md) is the exhaustive reference.

## Keys and values

A **key** is a node in the tree. It has an immutable GUID identity, a KACS [security descriptor](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md) that governs who can read or change it, and it holds values and child keys. You address a key by path and open it for specific `KEY_*` rights, getting back a key fd whose granted access is fixed for its lifetime.

A **value** is a named, typed piece of data on a key (`REG_SZ`, `REG_DWORD`, `REG_BINARY`, and the rest). An empty name is the key's *default* value.

## Layers and precedence — the LCS idea

Here is what makes LCS more than a key/value store. Every value write is tagged with a **layer**, and layers have a fixed precedence order. When you read a value, LCS resolves the **effective** entry — the one from the highest-precedence layer that has something to say — and that's what you get back.

This is what lets configuration compose cleanly:

- A base layer ships default configuration.
- A site or policy layer overlays organisation-wide settings.
- A machine-local layer overrides per-host.

They all coexist on the same key. Reading gives you the winner; writing targets a *specific* layer (or the base layer by default). And because it's layers rather than destructive overwrites, removing a higher layer's entry lets the lower one **re-emerge** — you can override and then un-override without losing the original.

**Tombstones** are the tool for "hide, don't delete": a per-value tombstone masks lower layers for one value, and a blanket tombstone masks all lower values of a key on a layer at once. Keys have the same idea via [hide](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#deleting-and-hiding-keys).

## Reads report where the answer came from

Because a value can come from any layer, the read tells you *which* layer won and gives you a **sequence number** for the effective entry. That sequence number is the basis of safe updates: pass it back as a compare-and-swap guard on a write, and the write only lands if nothing changed underneath you.

## Transactions

Mutating operations — creating keys, setting and deleting values — can be grouped into a **transaction** and committed atomically, so a multi-step configuration change either fully applies or not at all. Transactions are abort-by-default: close the transaction fd without committing and nothing happens. See [watching and transactions](/peios/developing-for-peios/sdk-registry/watching-and-transactions.md).

## Watches

You can ask a key to notify you when it changes — values, subkeys, or its security — optionally across its whole subtree. The elegant part: once armed, **the key fd itself becomes pollable**, so a registry watch drops straight into an `epoll` loop with no side channel.

## The client, not the source

This SDK is the registry **client** — it reads and writes the store. It does not implement a registry **source** (a storage backend); that's a separate library, [**librsi**](/peios/developing-for-peios/registry-sources/overview.md). As a client you speak only the calls in [`registry.h`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md).

## Where to go in this section

- **[Reading and writing](/peios/developing-for-peios/sdk-registry/reading-and-writing.md)** — open a key, read effective values, write to layers, do safe updates.
- **[Watching and transactions](/peios/developing-for-peios/sdk-registry/watching-and-transactions.md)** — react to changes and apply atomic multi-step edits.
- **[`registry.h` reference](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md)** — every call in full.

---

# Reading and writing

_Peios / Developing for Peios / The registry_

> Open a registry key, read its effective values, write into a layer, enumerate, and do lost-update-safe updates with compare-and-swap.

This guide covers the bread-and-butter registry operations. The full call signatures and error sets are in [`registry.h`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md); here we string them together. Recall the registry's [descriptor-struct buffer convention](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#the-buffer-convention-here): reads fill `*_cap`/`*_len` fields and a zero-capacity buffer probes.

The fragments below elide some error checks for brevity — every call returns `-1` with `errno` on failure, and real code must check each one (see [Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md)).

## Opening a key

Open a key for the rights you need:

```c
int key = peios_reg_open_key(-1, "/System/MyApp", KEY_READ, 0);
if (key < 0) {
    if (errno == ENOENT) { /* not there */ }
    else if (errno == EACCES) { /* not allowed */ }
    return -1;
}
```

`parent_fd == -1` means `path` is absolute. To open relative to a key you already hold, pass that key fd as the parent. Use [`peios_reg_create_key`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#opening-and-creating-keys) instead when the key might not exist yet — it opens-or-creates and reports which happened.

## Reading the effective value

Reading resolves layer precedence for you and hands back the winning value, its type, and which layer it came from:

```c
struct peios_reg_value v = {0};
unsigned char data[256];
v.data = data;  v.data_cap = sizeof data;
/* leave v.layer NULL if you don't care which layer won */

if (peios_reg_query_value(key, "Timeout", 7, -1, &v) == 0) {
    /* v.type is REG_*, v.data_len bytes valid in `data`, v.sequence is
       the effective entry's sequence number. */
} else if (errno == ENOENT) {
    /* no effective value (or a tombstone masks it) */
} else if (errno == ERANGE) {
    /* data buffer too small; v.data_len holds the required size — grow & retry */
}
```

The `name_len` is explicit (value names are length-counted; `0` reads the key's default value). Pass a transaction fd as the fourth argument to read within a transaction, or `-1` for none.

To read *every* value at once, use [`peios_reg_query_values_batch`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#enumerating-values) — one call fills a buffer with all effective values in a packed record format. To walk them one at a time, loop `peios_reg_enum_value` from index `0` until `ENOENT`.

## Writing a value

Writes target a specific layer. Pass `NULL`/`0` for the layer to write the base layer:

```c
uint32_t timeout = 30;
int rc = peios_reg_set_value(key, "Timeout", 7, REG_DWORD,
                             &timeout, sizeof timeout,
                             NULL, 0,      /* base layer */
                             -1,           /* auto-commit (no transaction) */
                             0);           /* no CAS guard */
```

Writing to a higher-precedence layer overrides lower ones without destroying them; [deleting](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#writing-deleting-tombstoning) that layer's entry later lets the lower value re-emerge.

## Safe updates with compare-and-swap

To read-modify-write without clobbering a concurrent change, feed the `sequence` you read back as the `expected_seq` guard on the write. The write only lands if nothing changed underneath you; otherwise it fails with `EAGAIN` and you retry:

```c
for (;;) {
    struct peios_reg_value v = {0};
    unsigned char buf[64]; v.data = buf; v.data_cap = sizeof buf;
    if (peios_reg_query_value(key, "Counter", 7, -1, &v) != 0)
        break;                                     /* real error — don't spin */

    uint32_t n; memcpy(&n, buf, sizeof n); n++;

    int rc = peios_reg_set_value(key, "Counter", 7, REG_DWORD,
                                 &n, sizeof n, NULL, 0, -1,
                                 v.sequence);          /* CAS on the sequence */
    if (rc == 0) break;                                /* success */
    if (errno != EAGAIN) { /* real error */ break; }   /* else: retry */
}
```

Passing `expected_seq == 0` disables the guard (an unconditional write).

## Cleaning up

Close key fds with `close()` when done. Values you wrote with auto-commit (`txn_fd == -1`) are already durable to the layer; to force the source to persist a hive's pending writes at a known point, call [`peios_reg_flush`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#watching-for-changes).

## Next

- **[Watching and transactions](/peios/developing-for-peios/sdk-registry/watching-and-transactions.md)** — react to changes and batch atomic edits.
- **[`registry.h` reference](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md)** — every call, field, and error.

---

# Watching and transactions

_Peios / Developing for Peios / The registry_

> React to registry changes through a pollable key fd, and apply multi-step edits atomically with transactions.

Beyond simple reads and writes, LCS gives you two higher-order tools: **watches** to react to change, and **transactions** to apply several edits atomically. Both are in [`registry.h`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md); this guide shows the shape of using them. To keep that shape visible, the fragments elide most error checks — every call returns `-1` with `errno` on failure, and real code must check each one (see [Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md)).

## Watching for changes

Ask a key to notify you when it changes, then poll its fd. Because the armed key fd is itself pollable, a watch integrates directly into whatever event loop you already run:

```c
int key = peios_reg_open_key(-1, "/System/MyApp", KEY_READ | KEY_NOTIFY, 0);

/* Watch values and subkeys, across the whole subtree. */
peios_reg_notify(key, REG_NOTIFY_VALUE | REG_NOTIFY_SUBKEY, 1 /* subtree */);

struct pollfd pfd = { .fd = key, .events = POLLIN };
for (;;) {
    poll(&pfd, 1, -1);
    if (pfd.revents & POLLIN) {
        /* read() the key fd to drain the change records, then re-read
           whatever you care about. */
        char records[512];
        ssize_t n = read(key, records, sizeof records);
        /* ... process change records ... */
    }
}
```

`filter` is a mask of `REG_NOTIFY_VALUE`, `REG_NOTIFY_SUBKEY`, and `REG_NOTIFY_SD`; `REG_NOTIFY_ALL` covers all three. The `subtree` flag extends the watch to descendants. Arming needs `KEY_NOTIFY` on the key. Call `peios_reg_notify(key, 0, 0)` to disarm.

Each `read()` drains as many complete change records as fit — every record starts `[total_len: u32][event_type: u16][name_len: u16][name]`, so you step through the buffer by `total_len`. The full layout, the `REG_WATCH_*` event types, and the extra path fields a subtree watch appends are in [the reference](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#watching-for-changes). Two practical notes: a buffer too small for even one record fails `EINVAL`, so size it generously rather than exactly; and a `REG_WATCH_OVERFLOW` record means events were dropped — re-read the key's state instead of trusting the stream.

This is how a service picks up configuration changes live — no polling loop re-reading values on a timer, just a blocking `poll` that wakes when something actually changed.

## Transactions

When a configuration change spans several operations — create a key, set a few values, delete a stale one — you usually want it to be all-or-nothing. That's a transaction.

Begin one, pass its fd as the `txn_fd` argument to each operation you want enlisted, then commit:

```c
int txn = peios_reg_begin_transaction();

int key = peios_reg_create_key(-1, "/System/MyApp/v2", KEY_WRITE, 0,
                               NULL /* base layer */, txn, NULL);
uint32_t one = 1;
peios_reg_set_value(key, "Enabled", 7, REG_DWORD, &one, sizeof one,
                    NULL, 0, txn, 0);
peios_reg_delete_value(key, "LegacyFlag", 10, NULL, 0, txn);

int rc = peios_reg_commit(txn);
if (rc == 0) {
    /* everything applied atomically; txn is terminal — close it */
} else if (errno == EBUSY || errno == EIO) {
    /* transient: the transaction is still active — retry the commit */
}
close(txn);
close(key);
```

Two things to keep in mind:

- **Abort is the default.** If you close the transaction fd without committing — including on any early-return error path — nothing is applied. So you don't need explicit rollback logic; just don't commit.
- **Commit can be retried.** `EBUSY` (write-lock contention) and `EIO` (source failure) leave the transaction **active**, so you can retry `peios_reg_commit`. Only `0` (committed — the fd is now terminal) and `EINVAL` (already committed or never bound) are final. Check state at any point with [`peios_reg_txn_status`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#transactions).

## Backup and restore

To snapshot a key and its whole subtree, or replace one from a snapshot, use [`peios_reg_backup`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md#backup-and-restore) and `peios_reg_restore`. They stream to and from an fd and are gated by `SeBackupPrivilege` / `SeRestorePrivilege`; restore applies in a single transaction.

## Next

- **[`registry.h` reference](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md)** — the notify filters, transaction states, and every error in full.
- **[Reading and writing](/peios/developing-for-peios/sdk-registry/reading-and-writing.md)** — the value operations you enlist in a transaction.

---

# Events overview

_Peios / Developing for Peios / Events_

> How KMES works from a developer's seat — the single event path, per-CPU ring buffers, trusted metadata, and MessagePack payloads.

KMES is Peios's event system, and it is the **sole** event path on the system. Audit records, subsystem events, and your own application events all travel the same way: the kernel stamps each event with trusted metadata and writes it into a per-CPU, lock-free ring buffer, and consumers drain those rings. This section teaches both sides — producing and consuming — via [`event.h`](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md) and [`msgpack.h`](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md).

## What an event is

An event has two parts:

1. **Kernel-stamped metadata** you cannot forge — a `CLOCK_REALTIME` timestamp, a per-CPU monotonic sequence number, the CPU id, an origin class, and identity GUIDs (the effective token, the true token, and the process). This is the trustworthy skeleton: when you consume an event, you *know* who emitted it and when, because the kernel wrote that, not the emitter.
2. **A payload** — a single [MessagePack](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md) value that you define. This is your event's actual content.

The `event_type` is a short UTF-8 string you choose, like `"my.app.login"`, that names the kind of event.

## Payloads are MessagePack, and you own them

The kernel does not build or interpret payloads — it only *structurally validates* them on emit (one well-formed MessagePack value, within size and nesting limits). So userspace owns encoding and decoding, and the SDK ships a [MessagePack codec](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md) whose validator's acceptance is matched to the kernel's check. Build a payload with the writer, and a successful `peios_mp_writer_bytes` (or `peios_mp_validate`) means the emit call will accept it.

## Per-CPU rings and lost events

Events live in **per-CPU** ring buffers — one ring per logical CPU, lock-free so producers never block on consumers. Two consequences shape how you consume:

- **You drain per CPU.** To see everything, run a reader per CPU (discover the count by attaching upward from CPU 0 until it fails).
- **Rings can lap.** If you don't drain fast enough, new events overwrite old ones you haven't read. The per-CPU `sequence` numbers are contiguous, so a **gap in the sequence means events were lost** — and the reader tracks that count for you.

## Privileges

The two sides are gated separately:

- **Emitting** requires `SeAuditPrivilege`.
- **Consuming** (attaching to a ring) requires `SeSecurityPrivilege`.

## Two ways to consume

The SDK offers a **high-level reader** that hides the entire lock-free drain — barriers, lapping recovery, lost-event accounting, buffer-resize handling, and the wait — behind a simple `next`/`wait` loop. That's what almost everyone should use. There is also a **low-level ring API** for callers who need to drive the drain inside their own event loop. Both are covered in [consuming events](/peios/developing-for-peios/sdk-events/consuming-events.md).

## Where to go in this section

- **[Emitting events](/peios/developing-for-peios/sdk-events/emitting-events.md)** — build a payload and emit, singly or in batches.
- **[Consuming events](/peios/developing-for-peios/sdk-events/consuming-events.md)** — drain the rings with the high-level reader (and, briefly, the low-level ring).
- **[`event.h`](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md)** and **[`msgpack.h`](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md)** — the exhaustive reference.

---

# Emitting events

_Peios / Developing for Peios / Events_

> Build a MessagePack payload and emit an event — singly, and in batches for high-rate producers.

Emitting an event is two steps: build a MessagePack payload, then hand it and an event type to the kernel. This guide shows both; [`event.h`](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md) and [`msgpack.h`](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md) are the full references. Emitting requires `SeAuditPrivilege`.

## Build the payload

Use the [MessagePack writer](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md#writer) to encode a single top-level value — typically a map of fields:

```c
peios_mp_writer *w = peios_mp_writer_new();

peios_mp_write_map(w, 2);                              /* {"user":…, "ok":…} */
peios_mp_write_str(w, "user", 4);  peios_mp_write_str(w, "alice", 5);
peios_mp_write_str(w, "ok", 2);    peios_mp_write_bool(w, true);

const void *payload;
ssize_t plen = peios_mp_writer_bytes(w, &payload);     /* validates as it borrows */
if (plen < 0) { /* EINVAL: malformed/under-filled — check peios_mp_writer_error */ }
```

`peios_mp_writer_bytes` validates that what you built is exactly one well-formed value, so a non-negative return means the payload is emit-ready. (Remember a map of `n` needs `2*n` values — one per key *and* value.)

## Emit it

```c
int rc = peios_event_emit("my.app.login", 12, payload, (uint32_t)plen);
peios_mp_writer_free(w);

if (rc != 0) {
    switch (errno) {
    case EPERM:  /* no SeAuditPrivilege */          break;
    case EINVAL: /* zero-length type or bad payload */ break;
    case ENOSPC: /* payload too large */            break;
    case EAGAIN: /* rate-limited — back off */      break;
    }
}
```

The event type is length-counted UTF-8 and must be non-zero length (`"my.app.login"` is 12 bytes — not NUL-terminated on the wire). On success the kernel stamps the trusted metadata (timestamp, sequence, identity GUIDs) and sets `origin_class = userspace`; you don't provide any of that.

### Validating untrusted payloads first

If a payload's shape comes from dynamic or untrusted input, validate it in userspace before emitting so you handle the failure on your terms rather than as an `EINVAL` from the kernel:

```c
if (peios_mp_validate(payload, plen, KMES_CONFIG_MAX_NESTING_DEPTH_DEFAULT) != 0) {
    /* reject it yourself */
}
```

The validator's acceptance matches the kernel's emit-time check at that depth bound.

## Emitting in batches

A high-rate producer should batch. [`peios_event_emit_batch`](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md#batch-emit) emits many events in one call, so a single timestamp capture, identity capture, and consumer wake cover the whole set:

```c
struct peios_event_entry entries[3] = {
    { "my.app.a", 8, pa, pa_len },
    { "my.app.b", 8, pb, pb_len },
    { "my.app.c", 8, pc, pc_len },
};

uint32_t emitted = 0;
int rc = peios_event_emit_batch(entries, 3, &emitted);
if (rc != 0) {
    /* errno is from entries[emitted] — the first that failed.
       entries[0..emitted) were emitted; resume from `emitted`. */
}
```

`count` must be in `[1, KMES_BATCH_MAX_ENTRIES]`. On failure, `errno` is the reason the first failing entry failed and `emitted` tells you how many succeeded before it, so you know exactly where to resume. One caveat: rate-limiting is **all-or-nothing** for a batch — an `EAGAIN` emits *none* of it, so on `EAGAIN` back off and retry the whole batch.

## Next

- **[Consuming events](/peios/developing-for-peios/sdk-events/consuming-events.md)** — the other side of the pipe.
- **[`msgpack.h` reference](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md)** — the full encoder, including containers, extensions, and raw splicing.

---

# Consuming events

_Peios / Developing for Peios / Events_

> Drain the per-CPU KMES rings with the high-level reader, parse payloads, track lost events, and know when to drop to the low-level ring API.

Consuming events means draining the per-CPU ring buffers. The SDK's high-level reader hides all the hard parts, so most consumers are a short loop. This guide shows that loop and how to parse what you read; [`event.h`](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md) has the full API. Consuming requires `SeSecurityPrivilege`.

## The reader loop

Open a reader for a CPU, then loop `next`/`wait`:

```c
peios_event_reader *r = peios_event_reader_open(cpu);
if (!r) { /* errno */ }

for (;;) {
    struct peios_event ev;
    int rc = peios_event_reader_next(r, &ev);
    if (rc == 1) {
        handle_event(&ev);                       /* got one */
    } else if (rc == 0) {
        peios_event_reader_wait(r, -1);          /* none right now — sleep */
    } else {
        break;                                   /* error */
    }
}
peios_event_reader_close(r);
```

`peios_event_reader_next` returns `1` (event filled), `0` (nothing available — call `wait`), or `-1` (error). `peios_event_reader_wait` blocks until events arrive or the timeout elapses (negative = forever). The reader handles the memory barriers, lapping recovery, buffer-resize handling, and futex wait internally — you just alternate the two calls.

## Parsing an event

Each `struct peios_event` gives you the trusted metadata by value and the payload as a MessagePack value. Parse it with a [reader](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md#reader):

```c
void handle_event(const struct peios_event *ev)
{
    /* Metadata is trustworthy — the kernel stamped it. */
    /* ev->timestamp, ev->sequence, ev->cpu_id, ev->origin_class,
       ev->process_guid, ev->effective_token_guid, ... */

    /* event_type and payload are NOT NUL-terminated — use the lengths. */
    if (ev->event_type_len == 12 &&
        memcmp(ev->event_type, "my.app.login", 12) == 0) {

        struct peios_mp_reader mp;
        peios_mp_reader_init(&mp, ev->payload, ev->payload_len);

        ssize_t pairs = peios_mp_read_map(&mp);
        for (ssize_t i = 0; i < pairs; i++) {
            const char *key; ssize_t klen = peios_mp_read_str(&mp, &key);
            /* dispatch on key; read or peios_mp_skip(&mp) the value */
        }
    }
}
```

### The lifetime rule

`ev->event_type` and `ev->payload` point **into the ring mapping**. They are valid only until your next `peios_event_reader_next` call, and only while the slot hasn't been overwritten. So **copy out anything you need to keep** before continuing the loop — don't stash the raw pointers.

## Draining every CPU

Rings are per-CPU, so one reader sees only one CPU's events. To consume the whole machine, run a reader per CPU — typically one thread each. Discover the CPU count by attaching upward until it fails:

```c
uint32_t ncpu = 0;
for (;;) {
    uint64_t cap;
    int fd = peios_event_attach(ncpu, &cap);   /* low-level probe */
    if (fd < 0) break;                          /* EINVAL past the last CPU */
    close(fd);
    ncpu++;
}
/* now spawn one peios_event_reader per cpu in [0, ncpu) */
```

## Watching for loss

If a consumer falls behind, the producer laps it and events are lost. Poll [`peios_event_reader_lost`](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md#the-high-level-reader) to see the cumulative count of lost events (derived from sequence gaps). A rising number means you aren't draining fast enough — process events more cheaply, hand off to a worker, or accept the loss deliberately.

## The low-level ring

If the reader's loop doesn't fit your event model — you want the ring integrated into an existing `epoll`/state-machine loop, driving the read position yourself — the [low-level ring API](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md#the-low-level-ring) exposes the mapping directly: `peios_event_ring_map`, the position accessors (`write_pos`/`tail_pos`/`generation`), `peios_event_ring_event_at` to parse a slot, and `peios_event_ring_wait` to sleep. It's more bookkeeping (you own the read position and the empty/lapping/generation checks) for more control. Reach for it only when you need to; the high-level reader is the right default.

## Next

- **[`event.h` reference](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md)** — the full reader and ring APIs, and every `struct peios_event` field.
- **[Auditing](/peios/security-fundamentals/auditing/overview.md)** — the operator-side view of the event and audit stream.

---

# Registry sources overview

_Peios / Developing for Peios / Registry sources_

> What a registry source is, how the RSI protocol flows, and the shape of a source's serve loop — the provider side of the registry, built with librsi.

A **registry source** is a storage backend for the [LCS registry](/peios/developing-for-peios/sdk-registry/overview.md). It is the other half of the registry story: where a [client](/peios/developing-for-peios/sdk-registry/overview.md) opens keys and reads values, a *source* is what actually holds those keys and values and answers the kernel when it needs them. If you are implementing a place for registry data to live — a file-backed store, a database adapter, an in-memory provider for tests — you are writing a source, and **librsi** is the library for it.

This is a different library from libpeios, for a different audience. libpeios is the registry *client*; librsi is the registry *source*. They share the SDK's [conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md) and substrate, but you link `librsi` (`-lrsi`, `<rsi.h>`) to write a source.

## How it works

A source talks to the kernel over the **RSI** protocol — the Registry Source Interface — on a single file descriptor:

```mermaid
flowchart LR
    client[Registry client<br/>libpeios] -->|syscalls| kernel[LCS in the kernel]
    kernel <-->|RSI framed protocol<br/>over the source fd| source[Your source<br/>librsi]
```

The flow is:

1. **Register.** Your process declares which *hives* (registry subtrees) it backs and registers with the kernel, receiving a **source fd**. This needs `SeTcbPrivilege`.
2. **Serve.** The kernel sends your source **requests** — "look up this child", "store this value", "begin this transaction" — as framed messages you `read(2)` from the source fd. You decode each one, do the work against your storage, and `write(2)` back a framed **response**.
3. **Repeat** until the source fd reaches EOF (the source is closing).

When a client reads or writes a key in one of your hives, the kernel turns that into RSI requests to *you*. Your source is the source of truth; the kernel mediates, enforces security, and resolves layer precedence on top of what you report.

## The serve loop

Every source is, at heart, this loop:

```c
for (;;) {
    ssize_t n = rsi_read_request(src_fd, buf, sizeof buf);   /* read a frame */
    if (n == 0) break;                                       /* EOF: closing */
    if (n < 0) { /* errno */ break; }

    struct rsi_request req;
    rsi_parse_request(buf, n, &req);                         /* split header/payload */

    switch (req.op_code) {                                   /* dispatch */
        case RSI_LOOKUP:     /* decode, resolve, rsi_respond_lookup */    break;
        case RSI_SET_VALUE:  /* decode, store,   rsi_respond_status */    break;
        /* … one case per op you support … */
    }
}
```

The three headers map onto the three steps:

- **[`<rsi/source.h>`](/peios/developing-for-peios/sdk-reference/sdk-rsi-source/rsi-source-h-becoming-a-source.md)** — registering and getting the source fd.
- **[`<rsi/request.h>`](/peios/developing-for-peios/sdk-reference/sdk-rsi-request/rsi-request-h-decoding-requests.md)** — reading and decoding requests.
- **[`<rsi/response.h>`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md)** — building and sending responses.

## What the kernel handles, and what you handle

The division of labour matters, because it keeps a source simple:

- **The kernel** handles security (access checks against key SDs), layer precedence resolution, transaction coordination across sources, and the client-facing syscall surface. It also assigns the global sequence numbers.
- **Your source** handles durable storage: it stores the name→GUID entries, the key metadata records, and the layered values, and it reports them back faithfully when asked. It honours transaction boundaries (buffer, then commit or abort) and compare-and-swap guards on writes.

You do **not** implement precedence, access control, or the client protocol — you store data and answer questions about it. That's why a source's serve loop is mostly a dispatch table over storage operations.

## Requests come in families

The [request reference](/peios/developing-for-peios/sdk-reference/sdk-rsi-request/rsi-request-h-decoding-requests.md) groups the operations, and it helps to hold the shape in mind:

- **Path/entry ops** — the name→GUID hierarchy (`LOOKUP`, `CREATE_ENTRY`, `HIDE_ENTRY`, `DELETE_ENTRY`, `ENUM_CHILDREN`).
- **Key ops** — key metadata records (`CREATE_KEY`, `READ_KEY`, `DROP_KEY`, `WRITE_KEY`).
- **Value ops** — the typed values on a key (`QUERY_VALUES`, `SET_VALUE`, `DELETE_VALUE_ENTRY`, `SET_BLANKET_TOMBSTONE`).
- **Transaction ops** — atomic grouping (`BEGIN`/`COMMIT`/`ABORT_TRANSACTION`).
- **Layer ops** — `DELETE_LAYER`, `FLUSH`.

Most reply with a simple status; five carry data back on success.

## Where to go in this section

- **[Registering a source](/peios/developing-for-peios/registry-sources/registering-a-source.md)** — declare your hives and get the source fd.
- **[Serving requests](/peios/developing-for-peios/registry-sources/serving-requests.md)** — the read/parse/dispatch/decode loop in full.
- **[Building responses](/peios/developing-for-peios/registry-sources/building-responses.md)** — status-only and payload-bearing replies.

---

# Registering a source

_Peios / Developing for Peios / Registry sources_

> Declare the hives your source backs and register with the kernel to obtain the source fd.

Every source starts by registering. You tell the kernel which hives your process backs, and it hands you a source fd to serve on. The full detail is in [`rsi/source.h`](/peios/developing-for-peios/sdk-reference/sdk-rsi-source/rsi-source-h-becoming-a-source.md); this guide walks the decisions.

## Declare your hives

A hive is a registry subtree with its own root key. Fill in one `struct rsi_hive` per hive your source backs:

```c
struct rsi_hive hive = {
    .name     = "MyStore",
    .name_len = 7,
    .flags    = 0,                    /* global hive */
    .root_guid = { /* 16-byte GUID of the hive root key */ },
    /* scope_guid left zero for a global hive */
};
```

The two decisions per hive:

- **Global or private?** A **global** hive (`flags = 0`, `scope_guid` zero) is visible system-wide. A **private** hive (`flags = RSI_HIVE_PRIVATE`, `scope_guid` non-zero) is scoped — only tokens carrying that scope GUID in their [LCS credentials](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md#lcs-registry-credentials) can resolve it. Use a private hive for per-application or per-tenant state that shouldn't be system-visible.
- **The root GUID.** Every hive is anchored at a root key identified by `root_guid`. This is the GUID paths in the hive resolve from, and it must match the root your storage actually holds.

## Register

Hand the kernel your hive array and the highest sequence number you've already persisted:

```c
int src = rsi_register(&hive, 1, /*max_sequence=*/0);
if (src < 0) {
    perror("rsi_register");   /* EPERM if you lack SeTcbPrivilege */
    return -1;
}
/* `src` is the source fd — serve the RSI protocol on it. */
```

Two things to get right:

- **`SeTcbPrivilege` is required.** Registration is a trusted operation; without the privilege you get `EPERM`. Sources run as trusted system components.
- **`max_sequence` is your durability contract.** It is the highest sequence number this source has *already persisted*. The kernel resumes its global sequence counter past it, so it never hands out a number you've used. A brand-new source with no stored state passes `0`; a source restarting with durable data must scan that data for the highest sequence it ever wrote and pass *that*. Getting this wrong risks sequence reuse, so make it the first thing your restart path computes.

You can register several hives in one call by passing an array and a `count` greater than one; the kernel enforces its configured `MaxHivesPerSource` limit (`ENOSPC` if you exceed it).

## After registration

The returned fd *is* your serve endpoint — you `read(2)` requests and `write(2)` responses on it directly (via the [request](/peios/developing-for-peios/sdk-reference/sdk-rsi-request/rsi-request-h-decoding-requests.md) and [response](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md) helpers). Closing it deregisters the source and signals EOF to the kernel. Keep it open for the life of the source, and move on to the [serve loop](/peios/developing-for-peios/registry-sources/serving-requests.md).

## Next

- **[Serving requests](/peios/developing-for-peios/registry-sources/serving-requests.md)** — the loop that runs on the source fd.
- **[`rsi/source.h` reference](/peios/developing-for-peios/sdk-reference/sdk-rsi-source/rsi-source-h-becoming-a-source.md)** — every field and error.

---

# Serving requests

_Peios / Developing for Peios / Registry sources_

> The source serve loop end to end — read a framed request, parse its header, dispatch on the op-code, and decode the payload with its typed parser.

With a [source fd in hand](/peios/developing-for-peios/registry-sources/registering-a-source.md), a source spends its life in one loop: read a request, decode it, do the work, reply. This guide builds that loop. The exhaustive per-op detail is in [`rsi/request.h`](/peios/developing-for-peios/sdk-reference/sdk-rsi-request/rsi-request-h-decoding-requests.md) and [`rsi/response.h`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md).

## The loop skeleton

```c
unsigned char buf[64 * 1024];   /* size generously — a short buffer is EMSGSIZE */

for (;;) {
    ssize_t n = rsi_read_request(src_fd, buf, sizeof buf);
    if (n == 0) break;                       /* EOF — the source is closing */
    if (n < 0) { perror("read_request"); break; }

    struct rsi_request req;
    if (rsi_parse_request(buf, n, &req) != 0) continue;   /* EBADMSG — skip */

    switch (req.op_code) {
        case RSI_LOOKUP:    handle_lookup(src_fd, &req);    break;
        case RSI_SET_VALUE: handle_set_value(src_fd, &req); break;
        /* … a case per op you support … */
        default:
            /* Unknown or unsupported op: reply with a non-OK status
             * (RSI_TXN_NOT_SUPPORTED for transaction ops you don't implement). */
            rsi_respond_status(src_fd, &req, RSI_INVALID);
            break;
    }
}
```

Three details in that skeleton matter:

- **`rsi_read_request` blocks** until a request is queued, and returns **`0` at EOF** — that's your exit. Size `buf` generously; a frame larger than `buf` returns `-1`/`EMSGSIZE`.
- **`rsi_parse_request` gives you `req.op_code`** to dispatch on, plus `req.request_id` (which every reply must echo — the helpers do this for you) and `req.txn_id` (nonzero when the request is inside a transaction).
- **Always reply.** Every request expects exactly one response. If you can't handle an op, reply with a non-OK status rather than dropping it.

## Decoding a request

Inside a handler, decode the payload with the matching `rsi_request_*` parser. For a `LOOKUP`:

```c
void handle_lookup(int fd, const struct rsi_request *req)
{
    struct rsi_lookup q;
    if (rsi_request_lookup(req, &q) != 0) {          /* EBADMSG */
        rsi_respond_status(fd, req, RSI_INVALID);
        return;
    }

    /* q.parent_guid (by value), q.child_name / q.child_name_len (borrowed).
       Resolve the child in your storage across its layers … */

    /* … then reply with the resolved path entries + metadata: */
    rsi_respond_lookup(fd, req, entries, entry_count, metadata, metadata_count);
}
```

And for a `SET_VALUE`, a status-only op:

```c
void handle_set_value(int fd, const struct rsi_request *req)
{
    struct rsi_set_value v;
    if (rsi_request_set_value(req, &v) != 0) {
        rsi_respond_status(fd, req, RSI_INVALID);
        return;
    }

    /* Honour the CAS guard before storing. */
    if (v.expected_sequence != 0 && current_seq(&v) != v.expected_sequence) {
        rsi_respond_status(fd, req, RSI_CAS_FAILED);
        return;
    }
    store_value(&v);                                  /* your storage */
    rsi_respond_status(fd, req, RSI_OK);
}
```

## The borrow rule

Every decoded name and data field — `q.child_name`, `v.value_name`, `v.data`, and the rest — is a `(ptr, len)` pair that **points into `buf`**. Those pointers are valid only until the next `rsi_read_request` reuses the buffer. So:

- **Consume them within the handler** (store the bytes, resolve the name) before the loop comes around again, or
- **Copy out** anything you need to keep. Never stash a raw borrowed pointer across iterations.

GUIDs and scalars in the decoded struct are copied by value, so those are always safe to keep — it's only the borrowed `(ptr, len)` fields that have the lifetime.

## Transactions

When the kernel sends `BEGIN_TRANSACTION`, buffer subsequent writes tagged with that `transaction_id` instead of applying them; `req.txn_id` on each later request tells you which transaction it belongs to (`0` = none). On `COMMIT_TRANSACTION` apply the buffered set atomically; on `ABORT_TRANSACTION` discard it. All three are status-only replies. The kernel coordinates transaction boundaries — your job is to buffer, then commit or discard on command.

## Next

- **[Building responses](/peios/developing-for-peios/registry-sources/building-responses.md)** — choosing and filling the right reply.
- **[`rsi/request.h` reference](/peios/developing-for-peios/sdk-reference/sdk-rsi-request/rsi-request-h-decoding-requests.md)** — every op's decoder and struct.

---

# Building responses

_Peios / Developing for Peios / Registry sources_

> Reply to RSI requests the right way — status-only for most ops and failures, payload-bearing helpers for the five ops that return data, and the validation contract to respect.

Every request gets exactly one response. Choosing the right one is simple once you know the rule; filling it in is a matter of handing librsi flat arrays and letting it encode the frame. The full contract is in [`rsi/response.h`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md); this guide is the working version.

## The rule

> **On failure, always [`rsi_respond_status`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md#status-only-responses). On success, `rsi_respond_status` too — unless the op is one of the five that carry a payload.**

Most operations (`SET_VALUE`, `CREATE_KEY`, the transaction ops, `FLUSH`, …) are status-only in both cases. And *any* op reports a non-OK outcome with `rsi_respond_status`, whatever it is:

```c
/* Success for a status-only op: */
rsi_respond_status(fd, req, RSI_OK);

/* Any op reporting a problem: */
rsi_respond_status(fd, req, RSI_NOT_FOUND);   /* e.g. a missing key */
rsi_respond_status(fd, req, RSI_CAS_FAILED);  /* a failed CAS */
```

You never build the frame or echo the request id yourself — the helper reads what it needs from `req` and writes the framed reply to `fd`.

## The five payload-bearing ops

Exactly five ops return data on success, each with its own helper:

| Op | Helper | You supply |
|---|---|---|
| `LOOKUP` | `rsi_respond_lookup` | path entries + the referenced keys' metadata |
| `ENUM_CHILDREN` | `rsi_respond_enum_children` | children (name + path entries) + metadata |
| `READ_KEY` | `rsi_respond_read_key` | one key's non-layered metadata |
| `QUERY_VALUES` | `rsi_respond_query_values` | value entries + blanket tombstones |
| `DELETE_LAYER` | `rsi_respond_delete_layer` | the orphaned keys' GUIDs |

You pass the result as flat arrays; librsi validates and heap-encodes the wire frame. A `QUERY_VALUES` reply, for example:

```c
struct rsi_value_entry values[] = {
    { .value_name = "Timeout", .value_name_len = 7,
      .layer_name = "base",    .layer_name_len = 4,
      .value_type = REG_DWORD, .data = &timeout, .data_len = 4,
      .sequence = 42 },
    /* … one per effective value you hold … */
};

rsi_respond_query_values(fd, req, values, 1, /*blankets=*/NULL, 0);
```

You report *what you store*, per layer; the kernel resolves precedence across the layers you return. (Note the `(NULL, 0)` for the empty blanket array — a NULL pointer is allowed only when its count is zero.)

## The validation contract

The helpers check their inputs before encoding and return `-1`/`EINVAL` if you break the contract — better a caught programming error than a malformed frame on the wire. The rules that apply across all of them:

- **`(ptr, len)` pairs:** a pointer may be `NULL` only when its length/count is zero.
- **Booleans** (`volatile_key`, `symlink`, target types) are strictly `0` or `1`.
- **Hidden path targets** (`RSI_PATH_TARGET_HIDDEN`) carry an **all-zero** `target_guid`.
- **`LOOKUP` / `ENUM_CHILDREN` metadata** must exactly cover the GUID targets in your path entries — every referenced key present, none missing, no duplicates, nothing unreferenced.
- **`DELETE_LAYER` orphan GUIDs** are nonzero and unique.

Beyond `EINVAL`, a helper can also fail with `ENOMEM` or `EOVERFLOW` (building the frame), `EIO` (a short write), or the raw `write(2)` errno — treat those as you would any I/O failure on the source fd.

## Putting it together

A source's handler for a payload-bearing op is: decode the request, gather the result from storage into the flat arrays the helper wants, call the helper. For a status-only op: decode, do the work, `rsi_respond_status`. On any error along the way — a decode failure, a missing key, an I/O problem — `rsi_respond_status` with the appropriate non-OK `RSI_*` code. That uniformity is what keeps a source's dispatch table readable no matter how many ops it supports.

## Next

- **[`rsi/response.h` reference](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md)** — every helper, struct, and error in full.
- **[Serving requests](/peios/developing-for-peios/registry-sources/serving-requests.md)** — the loop these replies live in.

---

# 1.1 Library conventions

_Peios / Developing for Peios / SDK Reference / Library Conventions_

> The handful of conventions that hold across every function in every libpeios module — learn them once and the rest reads as intent.

libpeios has a small number of conventions that hold across *every* function in *every* module. They are deliberately uniform: once you know how one function reports an error or returns a variable-length buffer, you know how all of them do. This page is the one to read slowly. Everything else in this documentation assumes it.

The conventions come in four groups: **how results are returned**, **the two-call buffer protocol**, **memory ownership** (builders and views), and **the small stuff** (file descriptors and constants).

---

# 1.2 How results are returned

_Peios / Developing for Peios / SDK Reference / Library Conventions_

> The three return shapes an entry point can have, and how each tells you where to read the result and how to detect failure.

Every entry point reports success or failure through its return type. There are three return shapes, and the shape tells you how to read the result.

### 1.2.0.1 `int` — a file descriptor, or zero

A function returning `int` returns either:

- a **file descriptor** (a non-negative `int`), when its job is to open something — a token, a registry key, an event stream; or
- **`0`** on success, when it performs an action with no handle to hand back; and
- **`-1` on failure**, with the reason in `errno`.

```c
int fd = peios_token_open_self(/* … */);
if (fd < 0) {
    /* errno is set — perror(), strerror(errno), etc. */
}
```

### 1.2.0.2 `ssize_t` — a byte length

A function returning `ssize_t` produces a **variable-length result** — a SID, a serialised security descriptor, a formatted string, a registry value. It returns:

- the **length in bytes** of the result on success (`>= 0`); or
- **`-1` on failure**, with the reason in `errno`.

These are the functions that use the [two-call buffer protocol](/peios/developing-for-peios/sdk-reference/sdk-conventions/the-two-call-buffer-protocol.md) below. The returned length is always the *full* length of the result, which is what makes the protocol work.

For functions that format a **string**, the returned length excludes the terminating `NUL` — exactly like `snprintf`. So a return of `41` means "41 characters plus a NUL"; size your buffer as `len + 1`.

### 1.2.0.3 Structured results — out-parameters

When a call produces more than one value, or a value that isn't naturally a length or an fd, it writes through **out-parameters** and returns `int` (`0` / `-1`). The access check is the archetype: it returns `0` when access is granted and `-1` with `errno == EACCES` when it is denied, and it writes the *granted access mask* through an out-parameter either way.

```c
uint32_t granted = 0;
int rc = peios_access_check(/* … */, &granted);
/* rc == 0: granted; rc == -1 && errno == EACCES: denied.
   `granted` is populated in both cases. */
```

A denial is a normal, expected outcome, not a bug — which is why it is reported the same disciplined way as any other errno, rather than through a separate channel.

### 1.2.0.4 errno

Failure is *always* reported through the standard C `errno`. The library sets `errno` on every `-1` return and uses ordinary, portable errno values — there are no libpeios-specific or PKM-specific error numbers to learn. The ones you will see most:

| errno | Meaning in libpeios |
|---|---|
| `EINVAL` | Malformed input — a bad SID, an unparseable SDDL string, an argument out of range. |
| `ERANGE` | Your output buffer was non-zero but too small. Nothing was written. (See the protocol below.) |
| `EACCES` | An access check denied the request. |
| `ENOMEM` | An allocation failed (for the heap-backed builders). |
| `EBADF`, `ESRCH`, `EFAULT` | The usual Linux meanings — a bad fd or pidfd, a vanished process, a bad pointer. |

Because the values are standard, `strerror`, `perror`, and your language's normal errno handling all work unchanged. Check the return value first, *then* read `errno` — like any POSIX call, `errno` is only meaningful after a call that signalled failure.

> Nothing ever unwinds across the boundary. The library is compiled to abort rather than propagate a panic through the C ABI, so a call either returns a value you can inspect or the process dies — it never leaves you with a corrupt half-state to reason about.

---

# 1.3 The two-call buffer protocol

_Peios / Developing for Peios / SDK Reference / Library Conventions_

> The getxattr-style protocol every variable-length function follows — measure with a null buffer, then call again with one big enough.

Every function that returns variable-length bytes — anything with an `ssize_t` return and an `(out, cap)` pair — follows the same **getxattr-style** protocol. It is the single most important convention in the library, so it is worth internalising.

The rule:

- Call with **`cap == 0`** (or a **`NULL` buffer**) to **probe**: the function writes nothing and returns the number of bytes the result needs.
- Call with a buffer of **at least that size** to **retrieve**: the function fills the buffer and returns the number of bytes it wrote.
- Call with a **non-zero but too-small** buffer and it **fails with `ERANGE` and writes nothing** — never a truncated or partial result.

That last point is the safety property that makes the protocol trustworthy: a too-small buffer is a clean, detectable error, not a silent truncation. You never have to wonder whether you got the whole thing.

The canonical two-call sequence:

```c
/* 1. Probe for the size. */
ssize_t need = peios_sid_format(sid, sid_len, NULL, 0);
if (need < 0) { /* errno set */ }

/* 2. Allocate. For a string, add 1 for the NUL. */
char *buf = malloc(need + 1);

/* 3. Retrieve. */
ssize_t n = peios_sid_format(sid, sid_len, buf, need + 1);
if (n < 0) { /* errno set */ }
/* buf now holds the formatted SID; n is its length (excluding the NUL). */
```

When you already know a comfortable upper bound, you can skip the probe and call once with a big-enough buffer. Some results have a fixed maximum the library gives you a constant for — for example a SID is never larger than `PEIOS_SID_MAX_BYTES`, so a stack buffer of that size always fits and never needs a probe. Those shortcuts are called out where they apply; the two-call protocol is always available as the general fallback.

---

# 1.4 Memory ownership

_Peios / Developing for Peios / SDK Reference / Library Conventions_

> libpeios never hands you an allocation to free — the builder pattern for constructing buffers, and the view pattern for reading them.

libpeios never hands you an allocation to `free()`. Instead it uses two ownership patterns — **builders** for constructing byte buffers and **views** for reading them — and both keep the memory question simple: you own your buffers, the library borrows or copies, and the two never get confused.

### 1.4.0.1 Builders — constructing buffers

Anything you *assemble* (an ACL, a security descriptor, a token specification) is built with a **builder**: an opaque, heap-backed object you create, feed, take the bytes from, and free.

Builders have three properties worth knowing up front:

1. **They are sticky-error.** The incremental `add`/`set` calls return `void` — they never fail inline. If one hits a problem (a bad input, an allocation failure), the builder **latches** the error and every later call is a no-op. You do not have to check each step. Instead you check *once*, at the end: either call the builder's `_error()` accessor (it returns the latched errno, or `0` if all is well), or notice that taking the bytes fails. This lets you write a long, clean sequence of `add` calls without a conditional after every line.

2. **You free every builder you create.** Each `_new()` is paired with a `_free()`. Builders also have a `_reset()` that drops the accumulated content *and* clears the sticky error, so you can reuse one builder across several objects instead of churning allocations.

3. **Taking the bytes: borrow (and sometimes copy).** Every builder has a **`_bytes()`** that hands back a pointer *into the builder* — zero-copy, no allocation. That pointer is valid only until the next mutating call, `_reset()`, or `_free()` on that builder. Use it when you are going to consume the bytes immediately (for instance, pass them straight into a kernel call). The call comes in two shapes, and not every builder offers a copying counterpart:
   - The **security builders** (`peios_acl_builder_bytes`, `peios_sd_builder_bytes`) *return the pointer* — `NULL` if the sticky error is set — and write the length through an optional `len_out` pointer. Each is paired with a **`_finish()`** that copies the buffer into a caller-supplied buffer using the [two-call protocol](/peios/developing-for-peios/sdk-reference/sdk-conventions/the-two-call-buffer-protocol.md) above, for when the bytes must outlive the builder.
   - **`peios_token_builder_bytes` and `peios_mp_writer_bytes`** are shaped the other way round: they *return the length* as an `ssize_t` (`-1` with `errno` on a latched error) and write the borrowed pointer through an out-parameter (which may be `NULL` to get just the length). Neither has a `_finish()` — copy the borrowed bytes yourself if they need to outlive the builder.

A typical builder lifecycle:

```c
peios_acl_builder *b = peios_acl_builder_new();   /* NULL on OOM */
peios_acl_builder_allow(b, sid, sid_len, mask, 0); /* void — no check */
peios_acl_builder_deny(b, other, other_len, mask, 0);

size_t len;
const void *acl = peios_acl_builder_bytes(b, &len); /* NULL if errored */
if (!acl) { int err = peios_acl_builder_error(b); /* handle */ }
/* … use `acl` before the next mutation … */

peios_acl_builder_free(b);
```

### 1.4.0.2 Views — reading buffers

Anything you *parse* (a security descriptor, an ACL, a SID array from a token) is read through a **view**: a small, caller-allocated struct that you point at a buffer you already hold.

Views have their own two rules:

1. **You allocate the view; it is stack-friendly.** A view type such as `peios_sd_view` is an opaque fixed-size struct — you declare one as a local variable and pass its address to the parse call. No heap, no free. The struct's fields are opaque: never read them directly; use the accessor functions.

2. **A view borrows the buffer it parses — zero-copy.** The parse call does not copy the data; the view points *into* your buffer, and every accessor that yields a SID, a nested ACL, or a blob hands back a pointer into that same buffer. So the buffer must **stay alive and unmodified** for as long as the view — and anything you derived from it — is in use. Free or mutate the underlying buffer and every pointer the view gave you dangles.

```c
peios_sd_view sd;                        /* on the stack */
if (peios_sd_parse(buf, buf_len, &sd) != 0) { /* EINVAL */ }

const void *owner; size_t owner_len;
if (peios_sd_view_owner(&sd, &owner, &owner_len) == 0) {
    /* `owner` points INTO `buf` — valid only while `buf` lives. */
}
```

Views compose: parsing a security descriptor gives you a `peios_sd_view`, from which you obtain a `peios_acl_view` for its DACL, from which you obtain each `peios_ace_view`. Every one of them borrows the *same* original buffer, so keeping that one buffer alive keeps the whole tree valid.

The symmetry is the thing to remember: **builders own heap and must be freed; views own nothing and borrow your buffer.** Constructing is builders, reading is views, and neither ever asks you to free something the library allocated.

---

# 1.5 File descriptors

_Peios / Developing for Peios / SDK Reference / Library Conventions_

> Every handle libpeios opens is a raw int file descriptor you close yourself, with the ordinary semantics that implies.

Handles that libpeios opens — tokens, registry keys, event streams — are **raw `int` file descriptors**, the same kind `open()` gives you. You close them with `close()`, poll them, and pass them across `exec` (or not) with the usual fd machinery.

They are created **`O_CLOEXEC` by default**: a handle does not leak across an `exec` unless you deliberately clear the flag with `fcntl`. This is the safe default for security-sensitive handles — a token or key fd will not silently end up in a child process you launch.

---

# 1.6 Constants

_Peios / Developing for Peios / SDK Reference / Library Conventions_

> libpeios does not rename the kernel's wire constants — where each set comes from, and why the spelling matches the headers.

libpeios does **not** invent its own names for the kernel's wire constants. The access-right bits, ACE types, control flags, and mapping structs all come straight from the `<pkm/*.h>` UAPI headers, and you use those published names directly: `KACS_ACCESS_*`, `KACS_ACE_TYPE_*`, `KACS_SD_*`, `struct kacs_generic_mapping`, and so on. There is no parallel `PEIOS_*` aliasing to translate in your head — the name in the PSD, the name in the kernel header, and the name you write in your code are the same name.

The handful of constants that *are* libpeios's own — buffer-size ceilings like `PEIOS_SID_MAX_BYTES`, and enums for convenience selectors like `enum peios_wks` (well-known SIDs) — are prefixed `PEIOS_` and documented with the module that defines them.

---

# 1.7 The conventions at a glance

_Peios / Developing for Peios / SDK Reference / Library Conventions_

> The whole convention set on one page, as a reference to come back to while reading the module chapters.

| Convention | The rule |
|---|---|
| `int` return | fd or `0` on success; `-1` + `errno` on failure. |
| `ssize_t` return | byte length on success; `-1` + `errno` on failure. Strings exclude the `NUL`. |
| Two-call protocol | `cap == 0` / `NULL` probes for the size; too-small non-zero buffer → `ERANGE`, nothing written. |
| errno | standard values only; check the return first, then `errno`. |
| Access denial | `-1` + `EACCES`, with the granted mask still written to the out-param. |
| Builders | heap-backed, sticky-error, `void` adders; check `_error()` at the end; `_free()` every one; `_bytes()` borrows, and the security builders add a `_finish()` that copies. |
| Views | caller-allocated (stack), opaque, borrow the parsed buffer; keep that buffer alive and unmodified. |
| File descriptors | raw `int`, `O_CLOEXEC` by default, closed with `close()`. |
| Constants | use the `<pkm/*.h>` `KACS_*` names directly; only libpeios's own additions are `PEIOS_*`. |

With these in hand, the module documentation reads as just "what does this function do?" — the *how* of memory and errors is answered here, once, for all of them. Next: [your first program](/peios/developing-for-peios/sdk-basics/your-first-program.md), which puts the protocol and the error model to work in something you can compile.

---

# 2.1 security.h — Security descriptors

_Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors_

> What security.h covers — SIDs, access masks, ACLs, descriptors, views, the SDDL codec and inheritance — and the conventions it assumes.

`<peios/security.h>` is the shared vocabulary of the whole access-control surface. SIDs, security descriptors, ACLs, and ACEs are the currency every KACS interface trades in — tokens carry them, files are protected by them, access checks evaluate them, and the registry secures keys with them. They cross the kernel boundary as variable-length, self-relative byte buffers in the MS-DTYP wire formats, and this module is the one place libpeios lifts that raw wire form into something safe to handle from C.

Everything here assumes the [library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md): `ssize_t` returns are byte lengths using the two-call protocol, builders are heap-backed and sticky-error, and views borrow the buffer they parse. This page does not repeat those rules per function — read that page first.

The module has four parts:

- **[SIDs](/peios/developing-for-peios/sdk-reference/sdk-security/sids.md)** — build, parse, format, and compare security identifiers.
- **[ACLs and security descriptors](/peios/developing-for-peios/sdk-reference/sdk-security/building-acls.md)** — assemble them with builders.
- **[Parsing](/peios/developing-for-peios/sdk-reference/sdk-security/parsing-views.md)** — read them back with zero-copy views.
- **[SDDL and inheritance](/peios/developing-for-peios/sdk-reference/sdk-security/sddl-text-codec.md)** — the text form and the userspace-only inheritance helpers.

The wire constants (`KACS_SID_*`, `KACS_SD_*`, `KACS_ACE_*`, and `struct kacs_generic_mapping`) come straight from `<pkm/sid.h>` and `<pkm/sd.h>`. libpeios does not re-alias them — you use the published ABI names directly.

## 2.1.1 See also

- **[Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md)** — the error, buffer, builder, and view rules this page builds on.
- **[SIDs](/peios/security-fundamentals/identity/sids.md)** and **[Security descriptors](/peios/security-fundamentals/security-descriptors/overview.md)** — the operator-side concepts behind this vocabulary.
- **[`<peios/token.h>`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md)**, **[`<peios/file.h>`](/peios/developing-for-peios/sdk-reference/sdk-files/file-h-file-security.md)**, **[`<peios/access.h>`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md)** — the KACS interfaces that consume this vocabulary, including the generic-mapping tables `peios_access_map_generic` expects.

---

# 2.2 SIDs

_Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors_

> Constructing, formatting and inspecting SIDs, the well-known set, and the integrity-level helpers.

A **SID** (Security Identifier) is the unique binary name of a principal. For the full account of what a SID *is* — its string and binary forms, the mixed endianness, the equality rule — see the operator-side page on [SIDs](/peios/security-fundamentals/identity/sids.md). This section is the API for handling them.

A SID is small and bounded. The largest possible encoding is `PEIOS_SID_MAX_BYTES` (68) bytes, so a buffer of that size holds any valid SID and the SID builders below never need a two-call probe — you can always pass a `PEIOS_SID_MAX_BYTES` stack buffer and skip straight to the retrieve call.

```c
#define PEIOS_SID_MAX_BYTES 68u
```

### 2.2.0.1 Constructing SIDs

Each of these encodes a SID into your buffer and returns its length (or `-1` with `errno`). Because a SID fits in `PEIOS_SID_MAX_BYTES`, the probe is optional — but these are still `ssize_t`/two-call functions, so passing `cap == 0` to probe works too.

| Function | Builds |
|---|---|
| `peios_sid_build(out, cap, id_authority, sub_auths, count)` | An arbitrary SID from its parts: a 48-bit identifier authority (numeric, encoded big-endian) and `count` sub-authorities (encoded little-endian). `count` is `0..KACS_SID_MAX_SUB_AUTHORITIES`. |
| `peios_sid_parse_string(out, cap, sddl)` | A binary SID from its SDDL string form (`"S-1-5-21-…"`). |
| `peios_sid_integrity(out, cap, level_rid)` | An integrity-label SID `S-1-16-<rid>` (see [`peios_integrity_level`](#integrity-levels)). |
| `peios_sid_logon(out, cap, session_id)` | A logon SID `S-1-5-5-<hi>-<lo>` from a 64-bit session id. |
| `peios_sid_well_known(out, cap, which)` | A well-known SID selected by [`enum peios_wks`](#well-known-sids). |

```c
ssize_t peios_sid_build(void *out, size_t cap, uint64_t id_authority,
                        const uint32_t *sub_auths, unsigned count);
ssize_t peios_sid_parse_string(void *out, size_t cap, const char *sddl);
ssize_t peios_sid_integrity(void *out, size_t cap, uint32_t level_rid);
ssize_t peios_sid_logon(void *out, size_t cap, uint64_t session_id);
ssize_t peios_sid_well_known(void *out, size_t cap, enum peios_wks which);
```

`peios_sid_build` fails with `EINVAL` if `count` exceeds the maximum, and (like all of these) with `ERANGE` if a non-zero `cap` is too small.

### 2.2.0.2 Formatting and inspecting SIDs

| Function | Returns |
|---|---|
| `peios_sid_format(sid, len, out, cap)` | The SDDL string form (`"S-1-…"`), as a string length excluding the `NUL` — allocate `len + 1`. |
| `peios_sid_valid(sid, len)` | `true` if `sid` is a structurally valid SID of *exactly* `len` bytes. |
| `peios_sid_length(sid)` | The encoded length of `sid`, read from its sub-authority count. **You must have already validated `sid`, or bounded it to `PEIOS_SID_MAX_BYTES`** — this trusts the buffer. |
| `peios_sid_equal(a, alen, b, blen)` | `true` for exact binary equality — the *only* equality KACS defines for SIDs. |
| `peios_sid_rid(sid, len)` | The RID (last sub-authority), or `0` if the SID has none. |

```c
ssize_t  peios_sid_format(const void *sid, size_t len, char *out, size_t cap);
bool     peios_sid_valid(const void *sid, size_t len);
size_t   peios_sid_length(const void *sid);
bool     peios_sid_equal(const void *a, size_t alen, const void *b, size_t blen);
uint32_t peios_sid_rid(const void *sid, size_t len);
```

The split between `peios_sid_valid` and `peios_sid_length` is deliberate: validation is the safe check that bounds an untrusted buffer; `peios_sid_length` is the fast reader you use *after* you trust the bytes (or when you have already capped the buffer at `PEIOS_SID_MAX_BYTES`). When in doubt, validate first.

### 2.2.0.3 Well-known SIDs

`peios_sid_well_known` constructs any of the standard system principals without you memorising their numbers:

```c
enum peios_wks {
    PEIOS_WKS_NULL,                 /* S-1-0-0    Nobody */
    PEIOS_WKS_EVERYONE,             /* S-1-1-0    World */
    PEIOS_WKS_LOCAL,                /* S-1-2-0    Local */
    PEIOS_WKS_CREATOR_OWNER,        /* S-1-3-0 */
    PEIOS_WKS_CREATOR_GROUP,        /* S-1-3-1 */
    PEIOS_WKS_OWNER_RIGHTS,         /* S-1-3-4    suppresses owner WRITE_DAC */
    PEIOS_WKS_ANONYMOUS,            /* S-1-5-7 */
    PEIOS_WKS_SELF,                 /* S-1-5-10   PRINCIPAL_SELF */
    PEIOS_WKS_AUTHENTICATED_USERS,  /* S-1-5-11 */
    PEIOS_WKS_SYSTEM,               /* S-1-5-18   Local System */
    PEIOS_WKS_LOCAL_SERVICE,        /* S-1-5-19 */
    PEIOS_WKS_NETWORK_SERVICE,      /* S-1-5-20 */
    PEIOS_WKS_ADMINISTRATORS,       /* S-1-5-32-544 */
};
```

For the meaning of each principal, see [Well-known principals](/peios/security-fundamentals/identity/well-known-principals.md).

### 2.2.0.4 Integrity levels

Integrity-label SIDs have the form `S-1-16-<rid>`, where the RID names a level. `peios_sid_integrity` takes that RID; the standard levels are:

```c
enum peios_integrity_level {
    PEIOS_IL_UNTRUSTED = 0,
    PEIOS_IL_LOW       = 4096,
    PEIOS_IL_MEDIUM    = 8192,
    PEIOS_IL_HIGH      = 12288,
    PEIOS_IL_SYSTEM    = 16384,
};
```

These are the labels that appear in a SACL as a `SYSTEM_MANDATORY_LABEL` ACE (see [`peios_acl_builder_label`](/peios/developing-for-peios/sdk-reference/sdk-security/building-acls.md#adding-aces)).

---

# 2.3 Access masks

_Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors_

> A 32-bit set of rights, the four generic bits that stand in for concrete ones, and how a generic mapping resolves them.

An access mask is a 32-bit set of rights. Masks may contain four *generic* bits (`KACS_ACCESS_GENERIC_READ/WRITE/EXECUTE/ALL`) that stand in for object-specific rights until they are mapped to a concrete object class.

```c
uint32_t peios_access_map_generic(uint32_t mask,
                                  const struct kacs_generic_mapping *m);
```

`peios_access_map_generic` folds the generic bits of `mask` into object-specific rights using the mapping `m`, and clears the generic bits from the result. Each object class publishes its canonical mapping as a data symbol you pass here — `peios_file_generic_mapping` (from [`<peios/file.h>`](/peios/developing-for-peios/sdk-reference/sdk-files/file-h-file-security.md)) and `peios_token_generic_mapping` (from [`<peios/token.h>`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md)). Use it when you have a mask written in generic terms (say, from an SDDL string using `GR`/`GW`) and need the concrete rights for a specific object type.

---

# 2.4 Building ACLs

_Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors_

> Assembling an ordered list of ACEs with an ACL builder — adding each ACE type, and taking the serialised bytes.

An **ACL** is an ordered list of ACEs. You assemble one with a `peios_acl_builder` — create it, add ACEs, take the serialised bytes, free it. Builders follow the [sticky-error rules](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#memory-ownership): the adders return `void`, the first error latches, and you check `peios_acl_builder_error` at the end.

```c
typedef struct peios_acl_builder peios_acl_builder;

peios_acl_builder *peios_acl_builder_new(void);   /* NULL on OOM */
void               peios_acl_builder_free(peios_acl_builder *b);
void               peios_acl_builder_reset(peios_acl_builder *b);
```

`peios_acl_builder_reset` drops every accumulated ACE *and* clears the sticky error, so you can reuse one builder for several ACLs.

### 2.4.0.1 Adding ACEs

The common single-SID families have convenience adders. `flags` is a mask of `KACS_ACE_FLAG_*` and is usually `0` — the flags carry inheritance semantics, which matter only for container/inheritable ACEs.

```c
void peios_acl_builder_allow(peios_acl_builder *b, const void *sid, size_t len,
                             uint32_t mask, uint8_t flags);
void peios_acl_builder_deny (peios_acl_builder *b, const void *sid, size_t len,
                             uint32_t mask, uint8_t flags);
void peios_acl_builder_audit(peios_acl_builder *b, const void *sid, size_t len,
                             uint32_t mask, uint8_t flags);
```

| Adder | Appends |
|---|---|
| `_allow` | An `ACCESS_ALLOWED` ACE — grants `mask` to `sid`. |
| `_deny` | An `ACCESS_DENIED` ACE — denies `mask` to `sid`. Order matters: put denies before allows. |
| `_audit` | A `SYSTEM_AUDIT` ACE — logs access by `sid` matching `mask`. Belongs in a SACL, not a DACL. |

For an integrity label there is a dedicated adder:

```c
void peios_acl_builder_label(peios_acl_builder *b, uint32_t integrity_rid,
                             uint32_t policy_mask);
```

It appends a `SYSTEM_MANDATORY_LABEL` ACE for integrity level `S-1-16-<integrity_rid>`. `policy_mask` is a mask of the `KACS_SYSTEM_MANDATORY_LABEL_NO_{READ,WRITE,EXECUTE}_UP` bits (from `<pkm/sd.h>`) that says which accesses a lower-integrity caller is denied. Like `_audit`, a label ACE belongs in a SACL.

For everything else — object ACEs, callback ACEs, resource-attribute ACEs — there is the general adder and a fully-specified ACE struct:

```c
struct peios_ace_spec {
    uint8_t       type;      /* KACS_ACE_TYPE_* */
    uint8_t       flags;     /* KACS_ACE_FLAG_* */
    uint32_t      mask;
    const void   *sid;       /* trustee */
    size_t        sid_len;
    const uint8_t *object_type;            /* 16-byte GUID, or NULL */
    const uint8_t *inherited_object_type;  /* 16-byte GUID, or NULL */
    const void   *app_data;  /* trailing callback/resource data */
    size_t        app_data_len;
};

void peios_acl_builder_add(peios_acl_builder *b, const struct peios_ace_spec *ace);
```

Fill in only the fields the `type` uses; leave the rest `NULL`/`0`:

- **Object ACEs** (`KACS_ACE_TYPE_*_OBJECT`) read `object_type` and `inherited_object_type` — each a 16-byte GUID, or `NULL` when absent.
- **Callback and resource-attribute ACEs** carry trailing `app_data` (which is `NULL` only when `app_data_len` is `0`). For callback ACEs this is the conditional-expression bytecode you can produce with [`peios_sddl_parse_condition`](/peios/developing-for-peios/sdk-reference/sdk-security/sddl-text-codec.md#conditional-expressions).

The convenience adders are exactly `peios_acl_builder_add` with a pre-filled spec for the common cases; reach for `_add` when you need object, callback, or resource-attribute ACEs.

### 2.4.0.2 Taking the ACL bytes

```c
const void *peios_acl_builder_bytes(peios_acl_builder *b, size_t *len_out);
ssize_t     peios_acl_builder_finish(peios_acl_builder *b, void *buf, size_t cap);
int         peios_acl_builder_error(const peios_acl_builder *b);
```

- `peios_acl_builder_bytes` borrows: it returns a pointer into the builder (valid until the next mutation, `_reset`, or `_free`), writing the length to `len_out` if non-`NULL`. It returns `NULL` if the sticky error is set.
- `peios_acl_builder_finish` copies the serialised ACL out using the two-call protocol.
- `peios_acl_builder_error` returns the latched errno, or `0` if the builder is healthy.

The usual next step is to hand these bytes to `peios_sd_builder_dacl` or `_sacl`.

---

# 2.5 Building security descriptors

_Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors_

> Binding an owner, group, DACL, SACL and control flags into one self-relative buffer with the descriptor builder.

A **security descriptor** binds an owner, a group, a DACL, a SACL, and control flags into one self-relative buffer. Its builder mirrors the ACL builder's shape.

```c
typedef struct peios_sd_builder peios_sd_builder;

peios_sd_builder *peios_sd_builder_new(void);
void              peios_sd_builder_free(peios_sd_builder *b);
void              peios_sd_builder_reset(peios_sd_builder *b);
```

### 2.5.0.1 Setting components

```c
void peios_sd_builder_owner(peios_sd_builder *b, const void *sid, size_t len);
void peios_sd_builder_group(peios_sd_builder *b, const void *sid, size_t len);
void peios_sd_builder_control(peios_sd_builder *b, uint16_t set, uint16_t clear);
void peios_sd_builder_dacl(peios_sd_builder *b, const void *acl, size_t len);
void peios_sd_builder_dacl_null(peios_sd_builder *b);
void peios_sd_builder_sacl(peios_sd_builder *b, const void *acl, size_t len);
```

- **Owner / group.** Omit the call to leave the component absent. That is exactly what you want when building a *partial* SD to set only some components via `kacs_set_sd` — the SD then carries only what you set.
- **Control bits.** `peios_sd_builder_control` sets the bits in `set` and clears those in `clear` (`KACS_SD_DACL_PROTECTED`, and friends). You do **not** manage `SELF_RELATIVE` or the `*_PRESENT` bits — the builder maintains those for you as you add components.
- **DACL / SACL.** Pass ACL bytes, typically straight from `peios_acl_builder_bytes`. An ACL with zero ACEs is a *present-but-empty* DACL, which grants only the owner's implicit rights.

The DACL has one subtlety worth stating plainly. KACS has **no NULL-DACL encoding** — there is no "DACL present, pointer null" form; the kernel's parser rejects it. So "grant everyone everything" is expressed as an **absent** DACL (the `DACL_PRESENT` control bit clear). `peios_sd_builder_dacl_null` requests exactly that: it clears any DACL you set earlier and produces the same bytes as never setting a DACL at all. It exists so you can state the grant-all intent explicitly rather than by omission — but be clear that it means *grant all*, not *deny all*.

### 2.5.0.2 Taking the SD bytes

Identical in shape to the ACL builder:

```c
const void *peios_sd_builder_bytes(peios_sd_builder *b, size_t *len_out);
ssize_t     peios_sd_builder_finish(peios_sd_builder *b, void *buf, size_t cap);
int         peios_sd_builder_error(const peios_sd_builder *b);
```

`_bytes` borrows (valid until the next mutation/reset/free, `NULL` if errored), `_finish` copies out getxattr-style, `_error` returns the latched errno.

---

# 2.6 Parsing — views

_Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors_

> Reading a descriptor, ACL or ACE without copying — the caller-allocated view structs and what each exposes.

To *read* a security descriptor, ACL, or ACE you use zero-copy **views**. A view is a caller-allocated, opaque, stack-friendly struct that borrows the buffer you parse — see the [view rules](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#memory-ownership). Every accessor that yields a SID, a nested ACL, or a blob returns a pointer *into the original buffer*, so that buffer must outlive the view and everything derived from it.

```c
typedef struct peios_sd_view        { uint64_t _opaque[8]; } peios_sd_view;
typedef struct peios_acl_view       { uint64_t _opaque[4]; } peios_acl_view;
typedef struct peios_ace_view       { uint64_t _opaque[4]; } peios_ace_view;
typedef struct peios_sid_array_view { uint64_t _opaque[4]; } peios_sid_array_view;
```

The `_opaque` arrays are sized for stack allocation with headroom — declare a view as a local and never read its fields.

### 2.6.0.1 Security-descriptor views

```c
int      peios_sd_parse(const void *sd, size_t len, peios_sd_view *out);
uint16_t peios_sd_view_control(const peios_sd_view *v);
int      peios_sd_view_owner(const peios_sd_view *v, const void **sid, size_t *len);
int      peios_sd_view_group(const peios_sd_view *v, const void **sid, size_t *len);
int      peios_sd_view_dacl(const peios_sd_view *v, peios_acl_view *out);
int      peios_sd_view_sacl(const peios_sd_view *v, peios_acl_view *out);
```

`peios_sd_parse` validates a self-relative SD and populates `out`, returning `0` or `-1` (`EINVAL`). `peios_sd_view_control` returns the raw control-bit word.

The four component accessors return `0` with their out-params set on success, or `-1` if the component is **absent**. For the DACL and SACL, `-1` also covers the NULL-DACL case — since an absent DACL and a NULL DACL are the same thing in KACS, a `-1` from `peios_sd_view_dacl` uniformly means "no DACL constrains this object."

### 2.6.0.2 ACL and ACE views

You can also parse a bare ACL directly — a token's default DACL, for instance, arrives as an ACL, not wrapped in an SD:

```c
int      peios_acl_parse(const void *acl, size_t len, peios_acl_view *out);
unsigned peios_acl_view_count(const peios_acl_view *a);
int      peios_acl_view_ace(const peios_acl_view *a, unsigned i, peios_ace_view *out);
```

`peios_acl_view_count` gives the number of ACEs; `peios_acl_view_ace` populates `out` for ACE `i` (0-based, in stored order), returning `0` or `-1` (`ERANGE` for an out-of-range index). Iterate in the obvious way:

```c
unsigned n = peios_acl_view_count(&dacl);
for (unsigned i = 0; i < n; i++) {
    peios_ace_view ace;
    peios_acl_view_ace(&dacl, i, &ace);
    /* inspect ace … */
}
```

Each ACE is read through its own accessors:

```c
uint8_t  peios_ace_view_type(const peios_ace_view *e);
uint8_t  peios_ace_view_flags(const peios_ace_view *e);
uint32_t peios_ace_view_mask(const peios_ace_view *e);
int      peios_ace_view_sid(const peios_ace_view *e, const void **sid, size_t *len);
int      peios_ace_view_object_type(const peios_ace_view *e, const uint8_t **guid16);
int      peios_ace_view_inherited_object_type(const peios_ace_view *e,
                                              const uint8_t **guid16);
int      peios_ace_view_app_data(const peios_ace_view *e, const void **data,
                                 size_t *len);
```

| Accessor | Yields |
|---|---|
| `_type` / `_flags` / `_mask` | The ACE's `KACS_ACE_TYPE_*` type, `KACS_ACE_FLAG_*` flags, and 32-bit access mask. |
| `_sid` | The trustee SID (a pointer into the buffer). `0` / `-1`. |
| `_object_type` | The object GUID of an object ACE — `0` with `*guid16` set to the 16 bytes, or `-1` if not present / not an object ACE. |
| `_inherited_object_type` | The inherited-object GUID, same convention. |
| `_app_data` | Trailing application data of a callback or resource-attribute ACE — for a callback ACE, this is the conditional-expression bytecode you can render with [`peios_sddl_format_condition`](/peios/developing-for-peios/sdk-reference/sdk-security/sddl-text-codec.md#conditional-expressions). |

### 2.6.0.3 SID-and-attributes arrays

Several token classes — `GROUPS`, `RESTRICTED_SIDS`, `DEVICE_GROUPS`, `CAPABILITIES` — return a packed `[count][sid_len][sid][attrs]…` blob rather than an ACL. Parse those with the SID-array view:

```c
int      peios_sid_array_parse(const void *blob, size_t len, peios_sid_array_view *out);
unsigned peios_sid_array_count(const peios_sid_array_view *a);
int      peios_sid_array_get(const peios_sid_array_view *a, unsigned i,
                             const void **sid, size_t *len, uint32_t *attrs);
```

`peios_sid_array_get` yields the `i`-th entry's SID (a pointer into the blob), its length, and its 32-bit attribute word (the `KACS_SE_GROUP_*` flags — enabled, mandatory, deny-only, and so on).

---

# 2.7 SDDL text codec

_Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors_

> Converting between the binary forms and human-readable SDDL text, entirely in userspace, including conditional expressions.

The SDDL codec converts between the binary wire forms above and their human-readable SDDL text (MS-DTYP §2.5.1). This is a **pure-userspace facility** — the kernel speaks only binary — so it lives entirely in libpeios. All four entries use the two-call protocol (`cap == 0` to probe) and fail with `EINVAL` on malformed input.

```c
ssize_t peios_sddl_parse_sd(void *out, size_t cap, const char *sddl);
ssize_t peios_sddl_format_sd(char *out, size_t cap, const void *sd, size_t sd_len);
```

- `peios_sddl_parse_sd` parses SDDL text (e.g. `"O:SYG:BAD:(A;;FA;;;BA)"`) into self-relative SD wire bytes.
- `peios_sddl_format_sd` renders SD wire bytes back to a NUL-terminated SDDL string (length excludes the `NUL`, so allocate `len + 1`).

These are the friendliest way to construct a descriptor when you have one written down — parse the string rather than assembling ACEs by hand — and the friendliest way to log or display one.

### 2.7.0.1 Conditional expressions

Callback ACEs carry a *conditional expression* as compiled "artx" bytecode. The codec converts between that bytecode and its SDDL expression text:

```c
ssize_t peios_sddl_parse_condition(void *out, size_t cap, const char *expr);
ssize_t peios_sddl_format_condition(char *out, size_t cap, const void *artx, size_t len);
```

- `peios_sddl_parse_condition` compiles an expression such as `@User.Title == "PM"` into the bytecode you place in a callback ACE's `app_data`.
- `peios_sddl_format_condition` renders bytecode back to text (with no outer parentheses), length excluding the `NUL`.

So the round trip for a conditional ACE is: write the condition as text → `peios_sddl_parse_condition` → put the bytecode in `peios_ace_spec.app_data` with a callback ACE `type` → add it to an ACL builder.

---

# 2.8 SD inheritance

_Peios / Developing for Peios / SDK Reference / security.h — SIDs and Descriptors_

> Computing a child object's ACEs from its parent's inheritable ones, in userspace, with both helper entry points.

Inheritance — computing a child object's ACEs from its parent's inheritable ones — is also pure userspace (MS-DTYP §2.5.3.4). Both helpers take and produce self-relative SDs and use the two-call protocol.

```c
ssize_t peios_sd_reinherit(void *out, size_t cap, const void *parent_sd,
                           size_t parent_len, const void *child_sd,
                           size_t child_len, int is_container);
ssize_t peios_sd_strip_inherited(void *out, size_t cap, const void *sd,
                                 size_t sd_len, uint32_t info);
```

**`peios_sd_reinherit`** recomputes a child SD's inherited ACEs from its parent. It strips the ACEs carrying `ACE_FLAG_INHERITED` from the child DACL, re-derives them from the parent DACL, and appends them *after* the child's explicit ACEs; the child's owner, group, SACL, and control bits pass through unchanged. `is_container` is non-zero if the child is itself a container (which determines how container-inherit and object-inherit flags propagate). This is what you call when a parent's ACL changed and you need to push the new inheritance down to a child.

**`peios_sd_strip_inherited`** drops the `ACE_FLAG_INHERITED` ACEs from the ACLs selected by `info` — a mask of `*_SECURITY_INFORMATION` bits, of which `DACL_SECURITY_INFORMATION` and `SACL_SECURITY_INFORMATION` are honoured and the rest ignored (selecting neither copies the input verbatim). Owner, group, and control bits pass through. Use it to reduce a descriptor to just its *explicit* ACEs — for example before storing a "protected" descriptor that should not carry inherited entries.

Both return the new SD's byte length, or `-1` with `EINVAL` (malformed input) or `ERANGE` (a non-zero buffer too small).

---

# 3.1 token.h — Tokens and sessions

_Peios / Developing for Peios / SDK Reference / token.h — Tokens_

> What token.h covers — opening, creating and adjusting tokens, querying them, and logon sessions — plus the constants it assumes.

`<peios/token.h>` is the token surface of KACS. A **token** is the runtime object that carries an identity — a user SID, group SIDs, privileges, an integrity level, claims — and every access decision is made against one. This module lets you open the tokens that already exist (your own, another process's, a socket peer's), mint new ones, read their contents, transform them, and install or impersonate them.

**A token handle is a file descriptor.** Every open/create/duplicate call returns a raw `int` fd, `O_CLOEXEC` by default, that you close with `close()`. The `access` argument several calls take is the desired *handle-right* mask (`KACS_TOKEN_*`), access-checked against the token's own security descriptor and cached on the fd — a handle only lets you do what its rights allow.

The wire constants (`KACS_TOKEN_*`, `KACS_IMLEVEL_*`, `KACS_SE_*_PRIVILEGE`, `KACS_TOKEN_CLASS_*`, `KACS_LOGON_TYPE_*`) and the ioctl arg structs (`kacs_priv_entry`, `kacs_group_entry`) come from `<pkm/token.h>`. Query payloads that are SID arrays or ACLs are read with the [views in `<peios/security.h>`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#parsing-views).

The module divides into: **opening & creating**, the **token-spec builder**, **query**, **adjust/transform**, and **logon sessions**.

## 3.1.1 See also

- **[`<peios/security.h>`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md)** — the SID/ACL/SD vocabulary and the views used to parse group and privilege query payloads.
- **[`<peios/access.h>`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md)** — checking access with a token fd.
- **[Tokens](/peios/security-fundamentals/tokens/overview.md)** and **[Impersonation](/peios/security-fundamentals/impersonation/overview.md)** — the operator-side model.

---

# 3.2 Opening and creating tokens

_Peios / Developing for Peios / SDK Reference / token.h — Tokens_

> The entry points that hand back a token fd — opening your own, another process's, or minting a new one.

Each of these returns a token fd (or `-1` with `errno`).

```c
int peios_token_open_self(unsigned flags, uint32_t access);
int peios_token_open_process(int pidfd, uint32_t access);
int peios_token_open_thread(int pidfd, int tid, uint32_t access);
int peios_token_open_peer(int conn_fd);
int peios_token_create_raw(const void *spec, size_t len);
```

| Function | Opens |
|---|---|
| `peios_token_open_self` | The calling thread's token. `flags` may be `KACS_TOKEN_OPEN_REAL` to get the **primary** token even while the thread is impersonating; otherwise you get the effective (impersonation-aware) token. `access` is the desired handle rights. |
| `peios_token_open_process` | The **primary** token of the process named by `pidfd`. Subject to a process-query access check and PIP dominance over the target. |
| `peios_token_open_thread` | Thread `tid`'s **impersonation** token if it is impersonating, else the process primary token. |
| `peios_token_open_peer` | The peer-identity token captured at `connect()` on a connected Unix stream/seqpacket socket `conn_fd` — how a server learns *who* is on the other end of a socket. The handle carries fixed `QUERY | IMPERSONATE` rights (no `access` argument). |
| `peios_token_create_raw` | Mints a token from a pre-built token-spec buffer. This is the escape hatch — **prefer the builder below**. Requires `SeCreateTokenPrivilege`. |

Errors, per call:

- **`peios_token_open_self`** — `EINVAL` (unknown `flags`; empty or unknown `access` bits), `EACCES` (the token's own SD denies `access`).
- **`peios_token_open_process`** — `EACCES` (any of the three checks failed — process-query right, PIP dominance, or the token SD; deliberately indistinguishable), `EBADF` (invalid pidfd), `ESRCH` (target exited), `EINVAL` (empty or unknown `access` bits).
- **`peios_token_open_thread`** — the `_open_process` set, plus `ESRCH` (thread exited, or not in `pidfd`'s process) and `EINVAL` (`tid <= 0`).
- **`peios_token_open_peer`** — `EACCES` (no captured peer token — an unconnected, datagram, or socketpair socket), `ENOTSOCK` (not a socket), `EBADF` (invalid fd).
- **`peios_token_create_raw`** — `EPERM` (privilege missing), `EINVAL` (spec failed kernel validation), `EFAULT` (bad spec pointer), `ENOMEM` (allocation failed).

`peios_token_open_peer` is the cornerstone of local authentication: accept a connection, open the peer token, and you have the caller's identity to query or impersonate — no password, no handshake, just the kernel's word for who connected.

---

# 3.3 The token-spec builder

_Peios / Developing for Peios / SDK Reference / token.h — Tokens_

> Assembling the token wire format with typed setters instead of by hand — core and advanced fields, flags, claims and credentials.

Minting a token means assembling a 192-byte-header wire format with many optional sections. The builder is the ergonomic path — typed setters, no hand-packed offsets — and follows the standard [sticky-error builder rules](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#memory-ownership): the setters return `void`, the first error latches, you check `peios_token_builder_error` at the end, and you `_free` every builder.

```c
typedef struct peios_token_builder peios_token_builder;

peios_token_builder *peios_token_builder_new(void);
void                 peios_token_builder_free(peios_token_builder *b);
void                 peios_token_builder_reset(peios_token_builder *b);
```

### 3.3.0.1 The index convention

Three fields — the owner, the primary group, and the restrict/deny indices — refer to SIDs *by index* into the token's own SID list rather than by value. The convention is fixed:

> **Index 0 is the user SID. Indices 1..N are the 1st..Nth group** you added with `peios_token_builder_add_group`, in order.

So to make the second group the primary group, you set `primary_group_index` to `2`. **Do not add the logon SID yourself** — the kernel injects it.

### 3.3.0.2 Core fields

```c
void peios_token_builder_user(peios_token_builder *b, const void *sid, size_t len);
void peios_token_builder_add_group(peios_token_builder *b, const void *sid,
                                   size_t len, uint32_t attrs);
void peios_token_builder_privileges(peios_token_builder *b, uint64_t present,
                                    uint64_t enabled);
void peios_token_builder_type(peios_token_builder *b, uint8_t type, uint8_t imp_level);
void peios_token_builder_integrity(peios_token_builder *b, uint32_t rid);
void peios_token_builder_session(peios_token_builder *b, uint64_t session_id);
void peios_token_builder_owner_index(peios_token_builder *b, uint32_t index);
void peios_token_builder_primary_group_index(peios_token_builder *b, uint32_t index);
void peios_token_builder_default_dacl(peios_token_builder *b, const void *acl, size_t len);
```

| Setter | Sets |
|---|---|
| `_user` | The user SID (index 0). |
| `_add_group` | Appends a group SID with its `KACS_SE_GROUP_*` attribute word (enabled, mandatory, deny-only, …). Call once per group, in the order you want them indexed. |
| `_privileges` | The privilege bitmasks: `present` (which privileges the token holds) and `enabled` (which are on). Bits are `KACS_SE_*_PRIVILEGE`. |
| `_type` | The token `type` (`KACS_TOKEN_TYPE_*` — primary or impersonation) and, for an impersonation token, the impersonation level `imp_level` (`KACS_IMLEVEL_*`). |
| `_integrity` | The integrity level, as the RID of an `S-1-16-<rid>` label (see [`peios_integrity_level`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#integrity-levels)). |
| `_session` | The logon session id the token references. |
| `_owner_index` / `_primary_group_index` | Which SID (by [index](#the-index-convention)) is the default owner / primary group. |
| `_default_dacl` | The default DACL applied to new objects the token creates (ACL bytes, e.g. from a [`peios_acl_builder`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#building-acls)). |

### 3.3.0.3 Advanced fields

These cover the rest of the token-spec and can be left unset. They are marked `[adv]` in the header for a reason — most tokens need none of them.

```c
void peios_token_builder_mandatory_policy(peios_token_builder *b, uint32_t bits);
void peios_token_builder_projected_ids(peios_token_builder *b, uint32_t uid, uint32_t gid);
void peios_token_builder_expiration(peios_token_builder *b, uint64_t when);
void peios_token_builder_source(peios_token_builder *b, const char name[8],
                                uint64_t source_id);
void peios_token_builder_audit_policy(peios_token_builder *b, uint32_t bits);
void peios_token_builder_add_restricted_sid(peios_token_builder *b, const void *sid,
                                            size_t len, uint32_t attrs);
void peios_token_builder_add_device_group(peios_token_builder *b, const void *sid,
                                          size_t len, uint32_t attrs);
void peios_token_builder_confinement(peios_token_builder *b, const void *sid, size_t len);
void peios_token_builder_supp_gids(peios_token_builder *b, const uint32_t *gids,
                                   unsigned count);
```

| Setter | Sets |
|---|---|
| `_mandatory_policy` | The mandatory-integrity policy bits governing how the integrity label is enforced. |
| `_projected_ids` | The POSIX `uid`/`gid` this token projects into the Linux-compatibility layer. |
| `_expiration` | An absolute expiry time after which the token is no longer valid. |
| `_source` | The token's source: an 8-byte `name` and a `source_id`, recording who issued it (appears in audit). |
| `_audit_policy` | Per-token audit policy bits. |
| `_add_restricted_sid` | Appends a restricting SID (a write-restricted / restricted token intersects these against the normal SIDs). |
| `_add_device_group` | Appends a device group SID (the device/machine side of a claim-aware token). |
| `_confinement` | The confinement/AppContainer package SID that sandboxes the token. |
| `_supp_gids` | Replaces the projected supplementary GIDs (pass `NULL, 0` to clear). |

### 3.3.0.4 Token flags

The four boolean token-spec flags are set together, so a designated initialiser reads clearly:

```c
struct peios_token_flags {
    bool write_restricted;
    bool user_deny_only;
    bool isolation_boundary;
    bool confinement_exempt;
};
void peios_token_builder_flags(peios_token_builder *b, const struct peios_token_flags *f);
```

- `write_restricted` — the token's restricting SIDs are checked only for write access.
- `user_deny_only` — the user SID is usable for deny ACEs but not to grant access.
- `isolation_boundary` — marks an isolation boundary for confinement.
- `confinement_exempt` — the token is exempt from confinement checks.

### 3.3.0.5 Claims

A **claim** is a named, typed, multi-valued security attribute — the input to conditional (callback) ACEs. Claims come in user and device flavours; both share the same shape.

```c
struct peios_token_claim_value {
    uint64_t    scalar;   /* INT64 / UINT64 / BOOLEAN (0 or 1) */
    const void *bytes;    /* STRING (UTF-8) / SID / OCTET */
    size_t      len;
};

struct peios_token_claim {
    const char *name;         /* UTF-8; transcoded to UTF-16LE on the wire */
    uint16_t    value_type;   /* KACS_CLAIM_TYPE_* */
    uint32_t    flags;        /* KACS_CLAIM_ATTR_* */
    const struct peios_token_claim_value *values;
    unsigned    value_count;
};

void peios_token_builder_add_user_claim(peios_token_builder *b,
                                        const struct peios_token_claim *claim);
void peios_token_builder_add_device_claim(peios_token_builder *b,
                                          const struct peios_token_claim *claim);
```

The `value_type` selects which member of each value carries the data:

| `value_type` | Value member |
|---|---|
| `KACS_CLAIM_TYPE_INT64` / `_UINT64` / `_BOOLEAN` | `scalar` (a boolean is `0` or `1`). |
| `KACS_CLAIM_TYPE_STRING` | `bytes`/`len` — a UTF-8 string (transcoded to UTF-16LE on the wire). |
| `KACS_CLAIM_TYPE_SID` | `bytes`/`len` — a binary SID. |
| `KACS_CLAIM_TYPE_OCTET` | `bytes`/`len` — an opaque blob. |

Each claim you add is round-tripped through the kernel's own claim parser before acceptance, so a malformed claim latches `EINVAL` on the builder immediately — you find out at build time, not at token-create time.

### 3.3.0.6 LCS registry credentials

The final optional section grants the token registry-layer powers: which layer scopes it may resolve and which private layers it owns.

```c
struct peios_token_lcs_credentials {
    const uint8_t (*scope_guids)[16];    /* array of 16-byte GUIDs, each non-nil & unique */
    unsigned    scope_count;             /* <= KACS_TOKEN_LCS_MAX_SCOPE_GUIDS */
    const char *const *private_layers;   /* UTF-8 names, 1..255 bytes, no '/' or '\\', unique */
    unsigned    private_layer_count;     /* <= KACS_TOKEN_LCS_MAX_PRIVATE_LAYERS */
};
void peios_token_builder_lcs_credentials(peios_token_builder *b,
                                         const struct peios_token_lcs_credentials *creds);
```

Setting it replaces any prior credentials; it is emitted as the last token-spec section. See [`<peios/registry.h>`](/peios/developing-for-peios/sdk-reference/sdk-registry-api/registry-h-the-registry-lcs.md) for what layers and scopes mean.

### 3.3.0.7 Finishing the builder

```c
ssize_t peios_token_builder_bytes(peios_token_builder *b, const void **out);
int     peios_token_builder_create(peios_token_builder *b);
int     peios_token_builder_error(const peios_token_builder *b);
```

- `peios_token_builder_bytes` returns the serialised length and, if `out` is non-`NULL`, writes a pointer into the builder (valid until the next reset/free) through it. Use this if you want the raw token-spec bytes.
- `peios_token_builder_create` does it in one step: serialise and mint, returning the new token fd. This is the usual call. It requires `SeCreateTokenPrivilege`.
- `peios_token_builder_error` returns the latched errno, or `0`.

Errors: `_bytes` and `_create` first surface any latched builder error — `EINVAL` (malformed field, SID, claim, or index) or `ENOMEM` (allocation failed). A clean `_create` then adds the `peios_token_create_raw` set: `EPERM` (privilege missing), `EINVAL` (spec failed kernel validation), `ENOMEM`.

```c
peios_token_builder *tb = peios_token_builder_new();
peios_token_builder_user(tb, user_sid, user_len);
peios_token_builder_add_group(tb, admins_sid, admins_len, KACS_SE_GROUP_ENABLED);
peios_token_builder_type(tb, KACS_TOKEN_TYPE_PRIMARY, 0);
peios_token_builder_integrity(tb, PEIOS_IL_MEDIUM);
peios_token_builder_session(tb, session_id);

int tok = peios_token_builder_create(tb);       /* -1 on failure */
if (tok < 0) { int e = peios_token_builder_error(tb); /* or errno */ }
peios_token_builder_free(tb);
```

---

# 3.4 Query

_Peios / Developing for Peios / SDK Reference / token.h — Tokens_

> Reading a token's contents by information class — the generic reader, and the typed wrappers over the common classes.

You read a token's contents by **information class**. The generic reader handles any class getxattr-style; typed convenience wrappers cover the common ones.

```c
ssize_t peios_token_query(int fd, uint32_t info_class, void *buf, size_t cap);
ssize_t peios_token_user(int fd, void *sid_buf, size_t cap);   /* CLASS_USER */
```

- `peios_token_query` reads the class `info_class` (`KACS_TOKEN_CLASS_*`) into `buf` using the [two-call protocol](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#the-two-call-buffer-protocol). Classes that return SID arrays or ACLs are parsed afterward with the [`<peios/security.h>` views](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#parsing-views) — e.g. read `CLASS_GROUPS` into a buffer, then `peios_sid_array_parse` it.
- `peios_token_user` is the same two-call read specialised to the user SID (`CLASS_USER`): probe with `sid_buf == NULL, cap == 0`, then retrieve.

For the common scalar classes there are typed helpers that write through a mandatory non-`NULL` out-pointer and return `0` / `-1`:

```c
struct peios_privilege_set {
    uint64_t present;
    uint64_t enabled;
    uint64_t enabled_by_default;
    uint64_t used;
};

int peios_token_type(int fd, uint32_t *out);            /* CLASS_TYPE */
int peios_token_session_id(int fd, uint32_t *out);      /* CLASS_SESSION_ID */
int peios_token_integrity(int fd, uint32_t *level_rid_out); /* CLASS_INTEGRITY_LEVEL */
int peios_token_privileges(int fd, struct peios_privilege_set *out); /* CLASS_PRIVILEGES */
```

`peios_token_privileges` returns all four privilege words at once: which privileges are `present`, which are `enabled`, which are `enabled_by_default`, and which have been `used` (the audit trail of privilege use).

Errors (all query calls): `EACCES` (handle lacks `QUERY`), `EINVAL` (unknown class), `ERANGE` (non-probe buffer too small), `EFAULT` (bad buffer pointer). The typed helpers add `EINVAL` (`NULL` out-pointer, or an unexpected payload shape).

---

# 3.5 Adjust and transform

_Peios / Developing for Peios / SDK Reference / token.h — Tokens_

> Changing a token in place or deriving a new one — privileges and groups, duplicate and restrict, impersonation and linked tokens.

These change a token or derive a new one from it. Deriving calls return a new fd; in-place adjustments return `0` / `-1`.

### 3.5.0.1 Privileges and groups

```c
int peios_token_adjust_privileges(int fd, const struct kacs_priv_entry *entries,
                                  unsigned count, uint64_t *prev_enabled);
int peios_token_reset_privileges(int fd);
int peios_token_adjust_groups(int fd, const struct kacs_group_entry *entries,
                              unsigned count, uint64_t *prev_state);
int peios_token_reset_groups(int fd);
```

- `peios_token_adjust_privileges` enables/disables the privileges named in `entries` (each a `kacs_priv_entry`); if `prev_enabled` is non-`NULL` it receives the prior enabled mask, so you can restore it later. `peios_token_reset_privileges` restores `enabled := enabled_by_default`. Errors: `EACCES` (handle lacks `ADJUST_PRIVILEGES`), `EINVAL` (empty or oversized batch, duplicate entry, enabling an absent privilege, unknown attribute bits), `EFAULT` (bad entries pointer).
- `peios_token_adjust_groups` is the group analogue. `prev_state`, if non-`NULL`, points at a caller array of `KACS_TOKEN_GROUP_MASK_WORDS` `uint64_t` words that receives the prior enabled bitmask. `peios_token_reset_groups` restores the default group state. Errors: `EACCES` (handle lacks `ADJUST_GROUPS`), `EINVAL` (mandatory, deny-only, or logon-SID group targeted; duplicate or out-of-range index; empty batch), `EFAULT` (bad entries pointer).

### 3.5.0.2 Duplicate and restrict

```c
int peios_token_duplicate(int fd, uint32_t access, uint8_t type, uint8_t imp_level);

struct peios_token_restrict {
    uint64_t           privs_to_delete;
    const uint32_t    *deny_group_indices;   /* groups demoted to deny-only */
    unsigned           deny_count;
    const void *const *restrict_sids;        /* added restricting SIDs */
    const size_t      *restrict_sid_lens;
    unsigned           restrict_count;
    uint32_t           flags;                /* KACS_TOKEN_RESTRICT_WRITE_RESTRICTED */
};
int peios_token_restrict(int fd, const struct peios_token_restrict *spec);
```

- `peios_token_duplicate` copies the token, returning a new fd with handle rights `access`, token `type` (`KACS_TOKEN_TYPE_*`), and impersonation level `imp_level` (`KACS_IMLEVEL_*`). This is how you turn a primary token into an impersonation token, or narrow a handle's rights. Errors: `EACCES` (handle lacks `DUPLICATE`, or the new token's SD denies `access`), `EINVAL` (unknown `type`/`imp_level`, raising an impersonation token's level, empty or unknown `access` bits), `ENOMEM` (allocation failed).
- `peios_token_restrict` creates a **filtered** token — the sandboxing primitive. It can delete privileges (`privs_to_delete`), demote groups to deny-only (`deny_group_indices`, by [index](/peios/developing-for-peios/sdk-reference/sdk-tokens/the-token-spec-builder.md#the-index-convention)), add restricting SIDs (`restrict_sids`/`restrict_sid_lens`), and set `KACS_TOKEN_RESTRICT_WRITE_RESTRICTED`. The result is a strictly less-powerful token you can hand to less-trusted code. Errors: `EACCES` (handle lacks `DUPLICATE`), `EINVAL` (duplicate or out-of-range deny index, malformed restricting SID, unknown `flags`, `NULL` spec or arrays), `ENOMEM` (allocation failed).

### 3.5.0.3 Impersonation and installation

```c
int peios_token_install(int fd);
int peios_token_impersonate(int fd);
int peios_token_revert(void);
```

- `peios_token_install` makes this **primary** token the calling process's primary token. Errors: `EACCES` (handle lacks `ASSIGN_PRIMARY`, or `SeAssignPrimaryTokenPrivilege` missing), `EINVAL` (not a primary token), `EAGAIN` (thread set changed mid-install — retry), `ENOMEM` (allocation failed).
- `peios_token_impersonate` makes this **impersonation** token the calling thread's effective identity — subsequent access checks on that thread run as the impersonated identity. Errors: `EACCES` (handle lacks `IMPERSONATE`), `EINVAL` (not an impersonation token), `EPERM` (restricted→unrestricted same-user — the one hard deny), `ENOMEM` (allocation failed).
- `peios_token_revert` undoes it: it clears the thread's impersonation token so checks run as the thread's real (primary) identity again. It takes no argument and is a no-op (reported as success) if the thread was not impersonating. This is the inverse of `peios_token_impersonate` — always pair them, ideally with `revert` in the cleanup path. Errors: none in normal operation.

The archetypal server flow: `peios_token_open_peer` the caller → `peios_token_impersonate` it → do the work as them → `peios_token_revert`.

### 3.5.0.4 Linked tokens and defaults

```c
int peios_token_link(int elevated_fd, int filtered_fd, uint64_t session_id);
int peios_token_get_linked(int fd);
int peios_token_adjust_default(int fd, const void *dacl, size_t len,
                               uint16_t owner_index, uint16_t group_index);
int peios_token_set_session_id(int fd, uint32_t session_id);
```

- `peios_token_link` links an elevated + filtered primary-token pair in `session_id` — the UAC-style split-token model, where a filtered token is the everyday identity and its elevated linked token is available on demand. `peios_token_get_linked` opens the linked token of `fd`, returning a new fd. Errors (`_link`): `EACCES` (`SeTcbPrivilege` missing, or either handle lacks `DUPLICATE`), `EINVAL` (self-link, role/session/user-SID mismatch, not primary tokens, unknown `session_id`, or an fd that is not a token fd), `EBADF` (invalid fd). Errors (`_get_linked`): `EACCES` (handle lacks `QUERY`), `ENOENT` (not part of a linked pair, or the pair was destroyed), `ENOMEM` (allocation failed).
- `peios_token_adjust_default` replaces the token's default DACL and/or owner/primary-group indices. `dacl == NULL` leaves the DACL unchanged (and ignores `len`); `dacl != NULL` with `len == 0` clears it; an index of `0xFFFF` leaves that index unchanged. Errors: `EACCES` (handle lacks `ADJUST_DEFAULT`), `EINVAL` (out-of-range index; malformed or oversized DACL), `EFAULT` (bad DACL pointer).
- `peios_token_set_session_id` sets the token's session id (requires `SeTcbPrivilege`). Errors: `EACCES` (handle lacks `ADJUST_SESSIONID`, or `SeTcbPrivilege` missing).

---

# 3.6 Logon sessions

_Peios / Developing for Peios / SDK Reference / token.h — Tokens_

> The lightweight kernel bookkeeping a token references, and the entry points for creating and destroying one.

A **logon session** is the lightweight kernel bookkeeping a token references — the "login" a token belongs to. Creating and destroying them requires `SeTcbPrivilege`.

```c
struct peios_session_spec {
    uint8_t     logon_type;     /* KACS_LOGON_TYPE_* */
    const char *auth_package;   /* UTF-8; may be "" */
    const void *user_sid;
    size_t      user_sid_len;
};

int peios_session_create(const struct peios_session_spec *spec, uint64_t *id_out);
int peios_session_destroy_empty(uint64_t session_id);
```

- `peios_session_create` creates a logon session of type `logon_type` (`KACS_LOGON_TYPE_*` — interactive, network, service, …) for `user_sid`, attributing it to `auth_package`. `id_out` is mandatory and receives the new session id, which you then pass to `peios_token_builder_session`. Errors: `EPERM` (`SeTcbPrivilege` missing), `EINVAL` (`NULL` spec, `id_out`, or field; malformed SID; oversized spec), `EFAULT` (bad pointer), `ENOMEM` (allocation failed).
- `peios_session_destroy_empty` destroys a session that has **no live tokens** — it fails rather than orphaning tokens. Clean up sessions only after every token referencing them is closed. Errors: `EPERM` (`SeTcbPrivilege` missing), `ENOENT` (no such session), `EBUSY` (live tokens, linked-pair state, or in-flight references).

---

# 3.7 The generic mapping

_Peios / Developing for Peios / SDK Reference / token.h — Tokens_

> The canonical generic-to-specific rights mapping for the token object class, exported for you to pass to an access check.

```c
extern const struct kacs_generic_mapping peios_token_generic_mapping;
```

The canonical generic→specific rights mapping for the **token** object class. Pass it to [`peios_access_map_generic`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#access-masks) or as the `mapping` in a [`peios_access_request`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md#the-request) when the object under check is a token.

---

# 4.1 access.h — Access checks

_Peios / Developing for Peios / SDK Reference / access.h — Access Checks_

> What access.h is for, the two things worth knowing before using it, and the conventions it assumes.

`<peios/access.h>` answers the central question of the whole access-control model: *may this subject perform this access on this object?* You hand it a token, a security descriptor, and a desired access mask, and it runs the full KACS AccessCheck pipeline and tells you whether access is granted and exactly which rights were granted.

Two things are worth saying up front:

- **These calls are advisory.** They *evaluate*, they do not *enforce*. `peios_access_check` tells you what the answer would be; enforcement of a real operation always runs inside the kernel against the subject's own process security block. Use these when *your* code is the resource manager — you hold an object, you have its security descriptor, and you need to make the grant/deny decision yourself.
- **A denial is a normal result, not an error.** Per the [library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#structured-results-out-parameters), a denied check returns `-1` with `errno == EACCES`, and the granted mask is still written out. Only a genuine failure (a bad token fd, a malformed SD) is an error in the usual sense.

## 4.1.1 See also

- **[`<peios/security.h>`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md)** — building the security descriptors and reading the generic-mapping tables this check consumes.
- **[`<peios/token.h>`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md)** — obtaining the `token_fd` to check, and `peios_token_generic_mapping`.
- **[Access decisions](/peios/security-fundamentals/access-decisions/overview.md)** — the operator-side account of how KACS reaches a grant/deny decision.

---

# 4.2 The request

_Peios / Developing for Peios / SDK Reference / access.h — Access Checks_

> The single struct describing every check — the core fields an ordinary check needs, and the advanced ones beneath them.

Every check is described by a single `struct peios_access_request`. Only the first block is needed for an ordinary check; everything below the divider is advanced and may be left zero/`NULL`. For every pointer/length pair, `NULL` is valid only when the matching length or count is zero.

```c
struct peios_access_request {
    int      token_fd;   /* -1 = the caller's effective token */
    const void *sd;      /* the object's security descriptor (wire bytes) */
    size_t   sd_len;
    uint32_t desired;    /* desired access mask */
    struct kacs_generic_mapping mapping;   /* the object class's mapping */

    /* ---- [adv] ---- */
    const void *self_sid;        /* PRINCIPAL_SELF substitution; NULL to omit */
    size_t   self_sid_len;
    uint32_t privilege_intent;   /* backup/restore intent bits */
    const struct kacs_object_type_entry *object_tree;
    uint32_t object_tree_count;
    const void *local_claims;    /* @Local claim array */
    size_t   local_claims_len;
    uint32_t pip_type;           /* 0 = use the subject's PSB */
    uint32_t pip_trust;
    const void *audit_context;   /* opaque object id for audit events */
    size_t   audit_context_len;
};
```

### 4.2.0.1 The core fields

| Field | Meaning |
|---|---|
| `token_fd` | The subject token to evaluate. **`-1` means the caller's own effective token** — the common case when you are checking access for yourself. Otherwise pass a token fd from [`<peios/token.h>`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md). |
| `sd` / `sd_len` | The object's security descriptor, as self-relative wire bytes — typically from a [`peios_sd_builder`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#building-security-descriptors) or read off the object. |
| `desired` | The access mask you want checked. May contain generic bits; the `mapping` resolves them. |
| `mapping` | The object class's generic mapping (a `struct kacs_generic_mapping`), so generic rights in `desired` and in the SD's ACEs fold to the right object-specific bits. Use the class's published table — e.g. `peios_file_generic_mapping` or `peios_token_generic_mapping`. |

### 4.2.0.2 The advanced fields

Leave these zero/`NULL` unless you need them:

| Field | Meaning |
|---|---|
| `self_sid` / `self_sid_len` | The SID to substitute for `PRINCIPAL_SELF` (`S-1-5-10`) in ACEs — the "self" the object belongs to. |
| `privilege_intent` | Backup/restore intent bits, letting `SeBackupPrivilege` / `SeRestorePrivilege` widen the granted mask as they would for a real backup or restore. |
| `object_tree` / `object_tree_count` | An object-type tree for a per-property check (object ACEs with type GUIDs). Mandatory for [`peios_access_check_list`](/peios/developing-for-peios/sdk-reference/sdk-access/the-object-type-list-variant.md). |
| `local_claims` / `local_claims_len` | An `@Local` claim array to evaluate conditional ACEs against, beyond the claims already on the token. |
| `pip_type` / `pip_trust` | Process-integrity-protection trust label to evaluate against; `pip_type == 0` uses the subject's own PSB. |
| `audit_context` / `audit_context_len` | An opaque object identifier stamped into any audit events the check generates. |

---

# 4.3 The check

_Peios / Developing for Peios / SDK Reference / access.h — Access Checks_

> Running the full AccessCheck pipeline from userspace, and how the granted mask and audit outputs come back.

```c
int peios_access_check(const struct peios_access_request *req,
                       uint32_t *granted, struct peios_access_audit *audit);
```

Runs the full AccessCheck pipeline. Returns:

- **`0`** if *every* right in `desired` is granted;
- **`-1` with `errno == EACCES`** if any desired right is denied;
- **`-1` with another errno** on a real error (e.g. `EBADF` for a bad `token_fd`, `EINVAL` for a malformed SD).

`granted`, if non-`NULL`, **always** receives the granted access mask — even on denial. This is the useful part: you can request a broad `desired` and read back exactly which subset was granted, rather than probing one right at a time. `audit`, if non-`NULL`, receives the [audit outputs](/peios/developing-for-peios/sdk-reference/sdk-access/audit-outputs.md).

```c
struct peios_access_request req = {
    .token_fd = -1,                      /* my own effective token */
    .sd = sd_bytes, .sd_len = sd_len,
    .desired = KACS_ACCESS_READ | KACS_ACCESS_WRITE,
    .mapping = peios_file_generic_mapping,
};

uint32_t granted = 0;
int rc = peios_access_check(&req, &granted, NULL);
if (rc == 0) {
    /* both READ and WRITE granted */
} else if (errno == EACCES) {
    /* denied; `granted` shows what WAS allowed (maybe READ only) */
} else {
    /* error: perror("access_check") */
}
```

libpeios owns the versioned `struct kacs_access_check_args` under the hood — it sets `caller_size` and zeroes the reserved fields so the request stays forward-compatible across kernel versions. You only ever fill in the `peios_access_request` above.

---

# 4.4 Audit outputs

_Peios / Developing for Peios / SDK Reference / access.h — Access Checks_

> The audit struct a check can fill in — continuous audit masks, staging mismatch, and what each field means.

```c
struct peios_access_audit {
    uint32_t continuous_audit;   /* OR of matching alarm masks */
    int      staging_mismatch;   /* 1 if the staged CAAP result differs */
};
```

When you pass a non-`NULL` `audit`, the check reports:

- `continuous_audit` — the OR of the alarm masks of any `SYSTEM_AUDIT` ACEs that matched, i.e. what a continuous-audit consumer would log for this access.
- `staging_mismatch` — `1` if evaluating the *staged* central access policy would have produced a different result than the active one. This is the signal you watch when rolling out a [central access policy](/peios/security-fundamentals/central-access-policies/overview.md) change: a non-zero value means the pending policy would decide this access differently.

---

# 4.5 The object-type-list variant

_Peios / Developing for Peios / SDK Reference / access.h — Access Checks_

> Checking a whole object type list in one call, with a per-node result for each entry.

```c
int peios_access_check_list(const struct peios_access_request *req,
                            struct kacs_node_result *results, uint32_t count);
```

`peios_access_check_list` is the `AccessCheckByTypeResultList` form — a *per-node* check over an object-type tree, for objects whose properties or property sets carry their own object ACEs (a directory-service-style object, say). It evaluates the whole tree in one call and reports a separate result for each node.

- `req->object_tree` / `object_tree_count` are **mandatory** here — they describe the tree of `kacs_object_type_entry` nodes to evaluate.
- `results` receives **one `kacs_node_result` per node, in preorder**, and `count` **must equal** `req->object_tree_count`.
- Returns `0` / `-1` (`EINVAL` if `count` doesn't match, and the usual errors otherwise).

Each `kacs_node_result` carries that node's granted mask and status, so you can discover, for example, that a caller may read most of an object but not one protected property — in a single check rather than one per property.

---

# 5.1 file.h — File security

_Peios / Developing for Peios / SDK Reference / file.h — File Security_

> Opening files with an explicit rights mask, reading and writing their descriptors, and the mount policy that covers filesystems without native storage.

`<peios/file.h>` is the file surface of KACS. Where ordinary POSIX `open()` gives you a file descriptor governed by mode bits, `peios_file_open` performs a **native** KACS open — an `NtCreateFile`-shaped call carrying a desired access mask, a create disposition, create options, and an optional creator security descriptor — and hands back an ordinary Linux file fd whose **granted access mask is fixed for the fd's lifetime**. Because the grant is baked into the fd, it can be delegated safely by `dup`, `SCM_RIGHTS`, or across `exec`: whoever holds the fd holds exactly the access it was opened with, no more.

Alongside the open, this module reads and writes a file's security descriptor (by path or by fd) and governs how a superblock without native SD storage is treated.

The wire constants (`KACS_DISPOSITION_*`, `KACS_CREATE_OPT_*`, `KACS_FILE_*`, `KACS_SECINFO_*`, `KACS_MOUNT_POLICY_*`, `KACS_STATUS_*`) come from `<pkm/file.h>` and `<pkm/sd.h>`. The security descriptors these calls exchange are built and parsed with [`<peios/security.h>`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md).

## 5.1.1 See also

- **[`<peios/security.h>`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md)** — building the creator SDs and parsing the SDs these calls return.
- **[`<peios/access.h>`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md)** — evaluating a file SD with `peios_file_generic_mapping`.
- **[File access](/peios/security-fundamentals/file-access/overview.md)** and **[Mount policies](/peios/using-peios/mount-policies/overview.md)** — the operator-side model of native file security.

---

# 5.2 Opening a file

_Peios / Developing for Peios / SDK Reference / file.h — File Security_

> The open params struct — desired access, disposition, share mode and the rest — and what the call returns.

```c
struct peios_open_params {
    uint32_t    desired_access; /* KACS_FILE_* | standard | generic (strict-mode) */
    uint32_t    disposition;    /* KACS_DISPOSITION_* */
    uint32_t    options;        /* KACS_CREATE_OPT_* */
    uint32_t    flags;          /* AT_SYMLINK_NOFOLLOW | KACS_BACKUP_INTENT | KACS_RESTORE_INTENT */
    const void *sd;             /* creator SD on create, else NULL */
    size_t      sd_len;
};

int peios_file_open(int dirfd, const char *path,
                    const struct peios_open_params *p, uint32_t *status_out);
```

`peios_file_open` opens `path` relative to `dirfd` (the usual `*at` convention — an absolute path ignores `dirfd`, and `AT_FDCWD` means the current directory). It returns a file fd, or `-1` with `errno`.

The parameters:

| Field | Meaning |
|---|---|
| `desired_access` | The access mask you are requesting — `KACS_FILE_*` object rights, standard rights, or (in strict mode) generic bits the file class maps. The granted subset is what the returned fd is fixed at. |
| `disposition` | What to do about existence: `KACS_DISPOSITION_*` — open-existing, create-new, open-or-create, supersede, overwrite, and so on. This is the create/open decision `open()` splits across `O_CREAT`/`O_EXCL`/`O_TRUNC`. |
| `options` | `KACS_CREATE_OPT_*` create options — directory-vs-file, no-follow, write-through, delete-on-close, and the rest of the `NtCreateFile` option set. |
| `flags` | `AT_SYMLINK_NOFOLLOW`, plus the privilege-intent flags `KACS_BACKUP_INTENT` / `KACS_RESTORE_INTENT` that let `SeBackupPrivilege` / `SeRestorePrivilege` widen the access the open is granted. |
| `sd` / `sd_len` | The **creator** security descriptor — the SD to stamp on a newly created file. Pass `NULL` when opening an existing file (or to let the parent's inheritance decide the new file's SD). |

`status_out`, if non-`NULL`, receives a `KACS_STATUS_*` code telling you *what happened* — whether the file was opened, created, superseded, overwritten. This is how you distinguish "created a new file" from "opened the existing one" after an open-or-create disposition, without a separate `stat` race.

Errors: `EACCES` (a requested right denied — strict mode), `EEXIST` (create-new and the file exists), `ENOENT` (open-existing and it doesn't), `ENOTDIR` (directory option, non-directory target), `ELOOP` (no-follow and the target is a symlink), `EINVAL` (`MAXIMUM_ALLOWED` without a concrete data/execute bit, malformed creator SD, `NULL` `path`/`p`, `sd == NULL` with `sd_len != 0`), `EBADF` (bad `dirfd`).

```c
struct peios_open_params p = {
    .desired_access = KACS_FILE_READ_DATA | KACS_FILE_WRITE_DATA,
    .disposition    = KACS_DISPOSITION_OPEN_IF,   /* open or create */
    .options        = 0,
    .sd             = creator_sd, .sd_len = creator_sd_len,
};
uint32_t status = 0;
int fd = peios_file_open(AT_FDCWD, "data.bin", &p, &status);
if (fd < 0) { /* errno */ }
/* status == KACS_STATUS_CREATED or KACS_STATUS_OPENED */
```

libpeios marshals these params into a `struct kacs_open_how` for you — setting its size and zeroing the reserved fields — so the call stays forward-compatible across kernel versions.

---

# 5.3 Reading and writing a file's security descriptor

_Peios / Developing for Peios / SDK Reference / file.h — File Security_

> Getting and setting a file's descriptor by path or by fd, with the secinfo mask that selects which components are touched.

A file's SD can be accessed **by path** or **by fd**. In both cases `secinfo` is a mask of `KACS_SECINFO_*` bits selecting which components (owner, group, DACL, SACL, …) the operation touches — you read or write just the parts you name and leave the rest alone.

The rights required scale with the components you touch (see [Managing file security](/peios/security-fundamentals/file-access/managing-file-security.md)):

| Component (`KACS_SECINFO_*`) | Reading needs | Writing needs |
|---|---|---|
| `OWNER` / `GROUP` | `READ_CONTROL` | `WRITE_OWNER` (plus owner-SID validation) |
| `DACL` | `READ_CONTROL` | `WRITE_DAC` |
| `SACL` | `ACCESS_SYSTEM_SECURITY` | `ACCESS_SYSTEM_SECURITY` |
| `LABEL` | `READ_CONTROL` | `WRITE_OWNER` (the label cannot rise above the caller's integrity without `SeRelabelPrivilege`) |

`ACCESS_SYSTEM_SECURITY` is itself gated by `SeSecurityPrivilege`; `READ_CONTROL` and `WRITE_DAC` are implicitly granted to the owner. `SACL` and `LABEL` cannot be combined in one call (`EINVAL`). The check is all-or-nothing: if any requested component fails its check, the whole call fails.

### 5.3.0.1 By path

```c
ssize_t peios_file_get_sd(int dirfd, const char *path, uint32_t secinfo,
                          void *buf, size_t cap, uint32_t at_flags);
int     peios_file_set_sd(int dirfd, const char *path, uint32_t secinfo,
                          const void *sd, size_t len, uint32_t at_flags);
```

- `peios_file_get_sd` reads the `secinfo`-selected components of `path`'s SD into `buf`, getxattr-style ([two-call protocol](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#the-two-call-buffer-protocol) — probe with `cap == 0`, and a too-small non-zero buffer fails `ERANGE` without truncating). `at_flags` accepts `AT_SYMLINK_NOFOLLOW`. Errors: `EACCES` (component right missing), `EINVAL` (`SACL` + `LABEL` together; `NULL` path, or `NULL` buffer with non-zero `cap`), `ERANGE` (non-probe buffer too small), `ENOENT` (path doesn't exist), `ELOOP` (no-follow and symlink).
- `peios_file_set_sd` writes the `secinfo` components of `sd` onto `path`, **preserving the components you did not select**. So to change only the DACL, build an SD with a DACL, pass `secinfo = KACS_SECINFO_DACL`, and the owner/group/SACL are untouched. Errors: `EACCES` (component right missing), `EPERM` (owner-SID validation failed without `SeRestorePrivilege`; label raised without `SeRelabelPrivilege`; MANDATORY attribute removed without `SeTcbPrivilege`), `EINVAL` (malformed SD, `SACL` + `LABEL` together, `NULL` or zero-length `sd`), `ENOENT`, `ELOOP`.

### 5.3.0.2 By fd

```c
ssize_t peios_fd_get_sd(int fd, uint32_t secinfo, void *buf, size_t cap);
int     peios_fd_set_sd(int fd, uint32_t secinfo, const void *sd, size_t len);
```

The same operations against the object `fd` already refers to. The access check they perform depends on the fd type: a normal file fd is checked against its **cached granted mask** (the one baked in at open), while an `O_PATH`, pidfd, or token fd triggers a **live** check. That distinction — cached for the fixed-grant file fd, live for the others — is documented in the Peios Kernel TRM §3.9, FACS; the practical upshot is that a file fd already opened with the right access can get/set its SD without a second path resolution.

The required rights and errors match the by-path calls, minus the path-resolution failures (`ENOENT`/`ELOOP`), plus `EBADF` (bad fd).

---

# 5.4 Mount policy

_Peios / Developing for Peios / SDK Reference / file.h — File Security_

> How KACS treats a superblock that cannot store native descriptors, and the entry points for reading and setting that policy.

Not every filesystem can store native security descriptors. The **mount policy** governs how KACS treats a superblock that has no native SD storage — whether files there get a synthesised SD, a template SD, or are denied. These calls target the superblock the object `fd` lives on and require `SeTcbPrivilege`.

```c
struct peios_mount_policy {
    uint32_t    policy;      /* KACS_MOUNT_POLICY_* */
    uint32_t    flags;
    uint32_t    generation;
    const void *template_sd;
    size_t      template_sd_len;
};

int peios_mount_get_policy(int fd, struct peios_mount_policy *out,
                           void *tmpl_buf, size_t tmpl_cap);
int peios_mount_set_policy(int fd, const struct peios_mount_policy *p);
```

- `peios_mount_get_policy` reads the policy for `fd`'s superblock into `out`. The template SD is returned into your `tmpl_buf` getxattr-style: on success `out->template_sd` points **into `tmpl_buf`** when that buffer was large enough, or is `NULL` if the superblock has no template. A `NULL` template buffer (or `tmpl_cap == 0`) is valid only when you don't need the template bytes. A too-small template buffer is **not** an error — the call still succeeds, reports the true length in `out->template_sd_len`, and leaves `out->template_sd` `NULL` so you can size a retry. Errors: `EPERM` (`SeTcbPrivilege` missing), `EBADF` (bad fd), `EINVAL` (`NULL` `out`, or `NULL` `tmpl_buf` with non-zero `tmpl_cap`), `EFAULT` (bad buffer pointer), `ENOMEM` (allocation failed).
- `peios_mount_set_policy` installs `p` as the superblock's policy. `policy` is a `KACS_MOUNT_POLICY_*` value; `template_sd`/`template_sd_len` supply the template SD when the policy calls for one. `flags` and `generation` must be zero on set — the kernel manages the generation counter itself and rejects a non-zero input. Errors: `EPERM` (`SeTcbPrivilege` missing), `EINVAL` (unknown or unmanaged `policy`, non-zero `flags`/`generation`, malformed or oversized template, `NULL` template with non-zero length), `EOPNOTSUPP` (superblock not KACS-managed), `EBADF` (bad fd), `EFAULT` (bad pointer).

---

# 5.5 The generic mapping

_Peios / Developing for Peios / SDK Reference / file.h — File Security_

> The canonical generic-to-specific rights mapping for the file object class, exported for you to pass to an access check.

```c
extern const struct kacs_generic_mapping peios_file_generic_mapping;
```

The canonical generic→specific rights mapping for the **file** object class. Pass it to [`peios_access_map_generic`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md#access-masks), or as the `mapping` in a [`peios_access_request`](/peios/developing-for-peios/sdk-reference/sdk-access/access-h-access-checks.md#the-request) when checking access against a file's SD — for example to pre-flight whether a caller could open a file before you actually open it.

---

# 6.1 process.h — Process security

_Peios / Developing for Peios / SDK Reference / process.h — Process Security_

> Complete reference for <peios/process.h> — setting process mitigation controls on the process security block (PSB).

`<peios/process.h>` is the process-security surface of KACS. Today it is a small module with a single job: turning on **process mitigations** — the hardening controls that live on a process's security block (PSB). More process-security surface will land here as it appears; for now, this is the mitigation control.

The mitigation bits are the `KACS_MIT_*` flags from `<pkm/psb.h>` (`KACS_MIT_WXP` through `KACS_MIT_SML`, with `KACS_MIT_ALL` as the mask of all valid bits). `KACS_MIT_CFI` is a legacy alias that expands to `KACS_MIT_CFIF | KACS_MIT_CFIB`. The full catalogue and semantics are in the Peios Kernel TRM §3.3, the Process Security Block.

## 6.1.1 Setting mitigations

```c
int peios_process_set_mitigations(int pidfd, uint32_t mitigations);
```

Turns on the mitigation bits named in `mitigations` (a mask of `KACS_MIT_*`). Returns `0` on success, or `-1` with `errno`.

Three properties define how this call behaves, and each matters:

- **It is one-way.** Mitigation bits can only be *set*, never cleared. Once a protection is on, it stays on for the life of the process. This is deliberate — a mitigation you could turn off is a mitigation an attacker could turn off — so treat each call as a permanent, additive commitment.
- **It targets a process by pidfd.** `pidfd == -1` targets the **calling** process, which is the common case: a program hardens itself early in startup. Targeting *another* process requires `PROCESS_SET_INFORMATION` on it **plus** PIP dominance over it — you cannot harden (or interfere with) a process you don't already dominate.
- **It is activation-backed and fails closed.** If a requested protection cannot actually be activated, the call **fails without mutating anything** — you never end up believing a mitigation is on when it isn't. Either every requested bit is activated and the call succeeds, or nothing changes and it returns `-1`.

```c
/* Harden the current process: enforce W^X and shadow-stack, refuse to
   proceed if either can't be activated. */
if (peios_process_set_mitigations(-1, KACS_MIT_WXP | KACS_MIT_SML) != 0) {
    perror("set_mitigations");
    /* nothing was changed; decide whether to continue unhardened or abort */
}
```

Because the call is all-or-nothing, request the bits you require together and check the result once: a success means the whole set is active, a failure means none of *this call's* bits were applied (bits set by earlier successful calls remain on).

## 6.1.2 See also

- **[`<peios/token.h>`](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md)** — PIP dominance is determined by the subject's token; process targeting other than self depends on it.
- **[Process mitigations](/peios/security-fundamentals/process-mitigations/overview.md)** — the operator-side account of each mitigation and what it defends against.

---

# 7.1 registry.h — The registry (LCS)

_Peios / Developing for Peios / SDK Reference / registry.h — The Registry_

> The registry client — opening keys, reading and writing values, enumerating, watching, securing, backing up and running transactions.

`<peios/registry.h>` is the client surface of **LCS** — the Layered Configuration Subsystem, Peios's kernel-mediated registry. LCS is modelled on the Windows registry: a hierarchy of **keys** (each with an immutable GUID identity and secured by its own KACS security descriptor) holding typed **values**. Its distinguishing feature is **layers**: every write is tagged with a precedence-ordered layer, and the *effective* view of a value resolves to the highest-precedence entry. That is what lets a base configuration, a site overlay, and a machine-local override coexist on one key and resolve deterministically.

This header is the registry **client**: open keys, read and write values, enumerate, watch, secure, back up, and run transactions. It does **not** cover the registry *source* (the storage backend) side — `REG_SRC_REGISTER` and the RSI framed protocol — which is a separate library, [**librsi**](/peios/developing-for-peios/registry-sources/overview.md). A client speaks only the syscalls and ioctls here.

**Handles are fds.** Three calls create file descriptors — `peios_reg_open_key`, `peios_reg_create_key`, and `peios_reg_begin_transaction`; everything else is an operation on a key fd or transaction fd, gated on the access right granted when the key was opened. The wire constants — value types (`REG_SZ` … `REG_QWORD`), key access rights (`KEY_*`), open/create flags, transaction states (`REG_TXN_*`), watch filters (`REG_NOTIFY_*`), and security-info bits — come from `<pkm/lcs.h>`.

## 7.1.1 See also

- **[`<peios/security.h>`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md)** — building and parsing the SDs that secure keys.
- **[Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md)** — the base error and buffer rules the descriptor reads specialise.
- **[The registry](/peios/using-peios/registry-concepts/overview.md)** — the operator-side model of layers, hives, and precedence.

---

# 7.2 The buffer convention here

_Peios / Developing for Peios / SDK Reference / registry.h — The Registry_

> Where the registry API departs from the library-wide two-call protocol, and what to do instead.

Most of libpeios returns variable-length data with an `ssize_t` and the [two-call protocol](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#the-two-call-buffer-protocol). The registry's *reads* use the same idea but express it through **descriptor structs** rather than a return value, because a single read often fills more than one buffer (a value's data *and* its layer name, say). The pattern:

- Each read takes a descriptor struct with `*_cap` fields (in) and `*_len` fields (out), plus buffer pointers.
- On success it returns `0` and writes the actual length into each `*_len`.
- If a buffer is too small it returns `-1` with `errno == ERANGE` and writes the **required** length into the matching `*_len` — so a **zero-capacity buffer probes the size**.
- A `NULL` buffer is valid only with zero capacity; `NULL` with a nonzero capacity is `EINVAL`.
- For a read with two buffers, `ERANGE` is returned if *either* is too small, and *both* required lengths are reported, so one probe sizes everything.

Everything else follows the usual Linux convention: `0` / `-1` + `errno`.

---

# 7.3 Opening and creating keys

_Peios / Developing for Peios / SDK Reference / registry.h — The Registry_

> Opening an existing key or creating one, relative to a parent fd, with a desired-access mask.

```c
int peios_reg_open_key(int parent_fd, const char *path, uint32_t desired_access,
                       uint32_t flags);
int peios_reg_create_key(int parent_fd, const char *path, uint32_t desired_access,
                         uint32_t flags, const char *layer, int txn_fd,
                         uint32_t *disposition_out);
```

Both resolve `path` (NUL-terminated) against `parent_fd` — a key fd for a relative path, or `< 0` for an absolute path — and return a key fd **whose granted access mask is fixed for its lifetime** (like a file fd, so it can be delegated). `desired_access` is the requested `KEY_*` rights, checked against the key's SD.

- **`peios_reg_open_key`** opens an *existing* key. `flags` may be `REG_OPEN_LINK` to open a symlink key *itself* rather than following it. Errors: `ENOENT`, `EACCES`, `EINVAL`, `ELOOP`, `ENAMETOOLONG`, `ETIMEDOUT`, `EIO`, `ENOMEM`.
- **`peios_reg_create_key`** opens an existing key or creates a new one. `flags` may combine `REG_OPTION_VOLATILE` (a key that does not survive reboot) and `REG_OPTION_CREATE_LINK` (create a symlink key). `layer` names the target layer to create in (NUL-terminated), or `NULL` for the base layer. `txn_fd` enlists the create in a [transaction](/peios/developing-for-peios/sdk-reference/sdk-registry-api/transactions.md), or `-1` to auto-commit. `disposition_out`, if non-`NULL`, receives `REG_CREATED_NEW` or `REG_OPENED_EXISTING`. Errors add `ENOSPC` and `EPERM` (privileged symlink creation) to the set above.

---

# 7.4 Values

_Peios / Developing for Peios / SDK Reference / registry.h — The Registry_

> Reading, writing, deleting and tombstoning a value, and enumerating a key's values — with names, types and layers.

A value is **named** (length-counted; an empty name is the key's *default* value), **typed** (`REG_*`), and written **into a layer**. A base-layer target is `layer == NULL` with `layer_len == 0`; a non-`NULL` pointer with a zero length is rejected `EINVAL`.

### 7.4.0.1 Reading a value

```c
struct peios_reg_value {
    uint64_t sequence;  /* out: effective entry's sequence number */
    void    *data;      /* in:  buffer for the value data (NULL to probe) */
    void    *layer;     /* in:  buffer for the layer name (NULL to probe/skip) */
    uint32_t type;      /* out: value type (REG_*) */
    uint32_t data_cap;  /* in */   uint32_t data_len;  /* out: actual/required */
    uint32_t layer_cap; /* in */   uint32_t layer_len; /* out: actual/required */
};

int peios_reg_query_value(int key_fd, const void *name, uint32_t name_len, int txn_fd,
                          struct peios_reg_value *v);
```

`peios_reg_query_value` reads the **effective** value `name` on `key_fd` — the winner of the layer precedence resolution. `name_len == 0` reads the default value; `txn_fd` reads within a transaction, or `-1` for none. It fills `v->data` with the value bytes and `v->layer` with the name of the layer that won, and reports the resolved `type` and `sequence`. Pass a `NULL` `layer` buffer if you don't care which layer won. Errors: `ENOENT` (no effective value, or a tombstone masks it), `ERANGE`, `EACCES`, `EINVAL`.

### 7.4.0.2 Writing, deleting, tombstoning

```c
int peios_reg_set_value(int key_fd, const void *name, uint32_t name_len, uint32_t type,
                        const void *data, uint32_t data_len, const void *layer,
                        uint32_t layer_len, int txn_fd, uint64_t expected_seq);
int peios_reg_delete_value(int key_fd, const void *name, uint32_t name_len,
                           const void *layer, uint32_t layer_len, int txn_fd);
int peios_reg_blanket_tombstone(int key_fd, const void *layer, uint32_t layer_len,
                                int set, int txn_fd);
```

- **`peios_reg_set_value`** writes value `name` of `type` into a specific `layer` (`NULL`/`0` = base). `type` may be `REG_TOMBSTONE` to place a *per-value* tombstone that masks lower layers. `expected_seq` is a **compare-and-swap guard**: `0` disables it; otherwise the write applies only if the value's current sequence matches, else `EAGAIN`. This is how you do lost-update-safe read-modify-write — read the `sequence` from `peios_reg_query_value`, then set with `expected_seq` set to it. Errors: `EINVAL`, `EAGAIN`, `ENOSPC`, `ENAMETOOLONG`, `EPERM`, `EACCES`.
- **`peios_reg_delete_value`** removes *a layer's* entry for `name` (`NULL`/`0` = base). It is idempotent, and removing a layer's entry lets any lower-layer value **re-emerge** — deletion is per-layer, not global.
- **`peios_reg_blanket_tombstone`** sets (`set != 0`) or clears (`set == 0`) a *blanket* tombstone on a layer, masking **all** lower-precedence values of this key on that layer at once — the wholesale version of a per-value tombstone. `set` must be `0` or `1` (else `EINVAL`).

### 7.4.0.3 Enumerating values

```c
int peios_reg_query_values_batch(int key_fd, int txn_fd, void *buf, uint32_t cap,
                                 uint32_t *len_out, uint32_t *count_out);

struct peios_reg_enum_value {
    void    *name;      /* in:  buffer for the value name (NULL to probe) */
    void    *data;      /* in:  buffer for the value data (NULL to probe) */
    uint32_t type;      /* out */
    uint32_t name_cap;  /* in */  uint32_t name_len;  /* out: actual/required */
    uint32_t data_cap;  /* in */  uint32_t data_len;  /* out: actual/required */
};
int peios_reg_enum_value(int key_fd, uint32_t index, int txn_fd,
                         struct peios_reg_enum_value *v);
```

Two ways to read every effective value of a key:

- **`peios_reg_query_values_batch`** reads them all into one `buf` in a single call — the efficient path. Each record is packed little-endian, back to back: `[name_len: u32][name][type: u32][data_len: u32][data]`, for `count` records. `len_out` receives the bytes written (or the required size on `ERANGE`); `count_out` receives the record count. Both may be `NULL`.
- **`peios_reg_enum_value`** reads one value at a time by `index`, dense over the key's tombstone-resolved values — walk from `0` until `ENOENT`. Use it when you want to process values incrementally rather than buffer them all.

---

# 7.5 Subkeys, metadata, and watches

_Peios / Developing for Peios / SDK Reference / registry.h — The Registry_

> Enumerating subkeys, reading key metadata, deleting and hiding keys, and arming a watch for changes.

### 7.5.0.1 Enumerating subkeys

```c
struct peios_reg_subkey {
    void    *name;             /* in:  buffer for the child's name (NULL to probe) */
    uint64_t last_write_time;  /* out: ns since the Unix epoch */
    uint32_t name_cap;         /* in */  uint32_t name_len;    /* out */
    uint32_t subkey_count;     /* out: the child's subkey count */
    uint32_t value_count;      /* out: the child's value count */
};
int peios_reg_enum_subkey(int key_fd, uint32_t index, int txn_fd,
                          struct peios_reg_subkey *v);
```

`peios_reg_enum_subkey` reads the child key at `index`, dense over visible children — walk from `0` until `ENOENT`. There is **no per-child access check** during enumeration (you see the names and counts; opening a child still checks its SD).

### 7.5.0.2 Key metadata

```c
struct peios_reg_key_info {
    void    *name;                 /* in:  buffer for the key's leaf name (NULL to probe) */
    uint64_t last_write_time;      /* out */
    uint64_t hive_generation;      /* out: per-hive change epoch */
    uint32_t name_cap;  uint32_t name_len;
    uint32_t subkey_count;         /* out */
    uint32_t value_count;          /* out */
    uint32_t max_subkey_name_len;  /* out */
    uint32_t max_value_name_len;   /* out */
    uint32_t max_value_data_size;  /* out */
    uint32_t sd_size;              /* out: security-descriptor size */
    uint8_t  volatile_key;         /* out: 1 if volatile */
    uint8_t  symlink;              /* out: 1 if a symlink */
};
int peios_reg_query_key_info(int key_fd, struct peios_reg_key_info *v);
```

`peios_reg_query_key_info` reads the key's leaf name and its metadata (needs `READ_CONTROL`). Note the ordering wrinkle: the kernel reports the metadata **only once the name fits**, so a too-small (or zero-capacity) name buffer returns `ERANGE` with the required `name_len` and *no* metadata — size the name buffer from that, then call again to get everything. The `max_*` fields are sizing hints for enumerations; `hive_generation` is a per-hive change epoch you can watch to detect that *anything* under the hive changed.

### 7.5.0.3 Deleting and hiding keys

```c
int peios_reg_delete_key(int key_fd, const void *layer, uint32_t layer_len, int txn_fd);
int peios_reg_hide_key(int key_fd, const void *layer, uint32_t layer_len, int txn_fd);
```

Both need `DELETE` access, take a layer (`NULL`/`0` = base) and an optional `txn_fd`, and cannot target a hive root (`EINVAL`).

- **`peios_reg_delete_key`** removes this key's path entry *in a layer*; lower-layer entries re-emerge. It fails with `ENOTEMPTY` if the key has visible children.
- **`peios_reg_hide_key`** creates a `HIDDEN` path entry that masks the key in a layer; removing that layer makes the key reappear. This is the key-level analogue of a tombstone — hide rather than destroy.

### 7.5.0.4 Watching for changes

```c
int peios_reg_notify(int key_fd, uint32_t filter, int subtree);
int peios_reg_flush(int key_fd);
```

- **`peios_reg_notify`** arms change watches on `key_fd` (needs `KEY_NOTIFY`). `filter` is a mask of `REG_NOTIFY_VALUE` / `REG_NOTIFY_SUBKEY` / `REG_NOTIFY_SD` (or `REG_NOTIFY_ALL`); `subtree` (`0`/`1`) extends the watch to descendants. `filter == 0` disarms. Once armed, **the key fd itself becomes pollable** — `EPOLLIN` signals pending events, and `read()` on the fd returns the change records. So a watch integrates directly into an `epoll` loop with no side channel. Errors: `ENOENT` (orphaned key), `EINVAL`, `EACCES`.
- **`peios_reg_flush`** forces the source to persist this key's hive's pending writes (needs `KEY_SET_VALUE`) and returns once persistence is confirmed — the durability barrier.

**The change records.** A `read()` on an armed key fd returns as many **complete** records as fit in your buffer — records are never split across reads. If the buffer is too small for even the next record the read fails `EINVAL` (so size it generously — a few KiB), and a non-blocking fd with nothing pending fails `EAGAIN`. Each record is a little-endian, possibly unaligned byte stream (Peios Kernel TRM §5.6, Watches, with the header offsets in §5.A):

| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 4 | `total_len` | Record size in bytes — advance by this to the next record (future versions may append fields). |
| 4 | 2 | `event_type` | `REG_WATCH_VALUE_SET` / `_VALUE_DELETED` / `_SUBKEY_CREATED` / `_SUBKEY_DELETED` / `_SD_CHANGED` / `_KEY_DELETED` / `_OVERFLOW`. |
| 6 | 2 | `name_len` | Byte length of `name`; `0` for the no-name events (`SD_CHANGED`, `KEY_DELETED`, `OVERFLOW`). |
| 8 | `name_len` | `name` | The changed value or subkey name (UTF-8, not NUL-terminated). |

A **subtree** watch appends two further fields after `name`: `path_depth` (`u16`) and that many length-prefixed path components (`u16` length + UTF-8 bytes), locating the changed key relative to the watched key — depth `0` means the watched key itself.

Delivery is best-effort with an overflow fallback: if records accumulate faster than you read them, the oldest are dropped and a `REG_WATCH_OVERFLOW` record is queued — on seeing one, re-read the watched key (and subtree) to recover current state rather than trusting the stream. Records describe **effective** (layer-resolved) changes, and uncommitted transactions produce none — events fire at commit.

---

# 7.6 Key security descriptors

_Peios / Developing for Peios / SDK Reference / registry.h — The Registry_

> Getting and setting a registry key's security descriptor, with the same secinfo mask the file API uses.

```c
int peios_reg_get_security(int key_fd, uint32_t security_info, void *sd, uint32_t cap,
                           uint32_t *sd_len_out);
int peios_reg_set_security(int key_fd, uint32_t security_info, const void *sd,
                           uint32_t sd_len, int txn_fd);
```

Keys are KACS-secured, so their SDs are read and written with the same [`<peios/security.h>`](/peios/developing-for-peios/sdk-reference/sdk-security/security-h-security-descriptors.md) vocabulary as files and tokens; `security_info` selects components (owner/group/DACL/SACL).

- **`peios_reg_get_security`** reads the selected components into `sd` (KACS binary form), writing the length to `*sd_len_out` (may be `NULL`); a too-small buffer returns `ERANGE` with the required size there, and a zero `cap` probes. Owner/group/DACL need `READ_CONTROL`; the SACL needs `ACCESS_SYSTEM_SECURITY`.
- **`peios_reg_set_security`** applies the selected components of `sd`, merging with the rest (the kernel parses and validates). The DACL needs `WRITE_DAC`, the owner `WRITE_OWNER`, the SACL `ACCESS_SYSTEM_SECURITY`. Here `txn_fd` gives **atomicity, not layer qualification** (SDs are not layered), or `-1` to apply immediately. SD changes affect only **future** opens — handles already open keep their fixed grant.

---

# 7.7 Backup and restore

_Peios / Developing for Peios / SDK Reference / registry.h — The Registry_

> Streaming a key and everything beneath it out to a descriptor, and restoring it back.

```c
int peios_reg_backup(int key_fd, int output_fd);
int peios_reg_restore(int key_fd, int input_fd);
```

- **`peios_reg_backup`** exports the key and its entire subtree to `output_fd` (needs `SeBackupPrivilege`). It takes a read-only snapshot and performs no per-key access check — the privilege is the gate. Errors: `EPERM`/`EACCES`, `EBADF` (output not writable), `ENOENT`, `ENOTSUP`, `EBUSY`.
- **`peios_reg_restore`** replaces the key and its entire subtree from `input_fd` (needs `SeRestorePrivilege`), applied in **one transaction**. Errors: `EPERM`/`EACCES`, `EBADF` (input not readable), `EINVAL` (malformed stream), `EEXIST` (GUID collision), `EOVERFLOW`.

---

# 7.8 Transactions

_Peios / Developing for Peios / SDK Reference / registry.h — The Registry_

> Batching key creates and mutating operations into an atomic unit — beginning, committing and aborting one.

A transaction batches key creates and mutating value/key operations into an atomic unit.

```c
int peios_reg_begin_transaction(void);
int peios_reg_commit(int txn_fd);
int peios_reg_txn_status(int txn_fd, uint32_t *state_out, int *terminal_errno_out);
```

- **`peios_reg_begin_transaction`** starts one and returns a transaction fd (initially unbound; it binds to a source on first use), or `-1`/`ENOMEM`. Pass this fd as the `txn_fd` argument to the create and mutating calls to enlist them. **Closing the fd without committing aborts** the transaction — so a transaction is abort-by-default, which makes error paths safe.
- **`peios_reg_commit`** atomically applies everything enlisted. On success the fd is **terminal** — close it. Errors tell you what to do: `EINVAL` (already committed / never bound), `EBUSY` (write-lock contention — the transaction **stays active**, retry the commit), `EIO` (source failure — stays active), `ETIMEDOUT`.
- **`peios_reg_txn_status`** reads a transaction's state: `state_out` receives the `REG_TXN_*` state, and `terminal_errno_out` receives the errno that ended it (`0` while active or after a clean commit). Both may be `NULL`.

The lifecycle: `begin` → enlist operations by passing `txn_fd` → `commit` (retry on `EBUSY`/`EIO`) → `close`, or just `close` to abort.

---

# 8.1 event.h — Events (KMES)

_Peios / Developing for Peios / SDK Reference / event.h — Events_

> Emitting events into KMES and consuming them from the ring buffers, with MessagePack payloads built by the codec module.

`<peios/event.h>` is the client surface of **KMES** — Peios's sole event path. The kernel stamps every event with **trusted metadata** (timestamp, per-CPU sequence, CPU id, identity GUIDs) and writes it into a **per-CPU lock-free ring buffer**. There is no other way to emit or observe events: audit records, subsystem events, and your own application events all flow through the same rings. Producers emit; consumers attach to the rings and drain them.

Each event payload is **a single MessagePack value** — build and parse it with [`<peios/msgpack.h>`](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md).

Two privileges gate the module: **emitting requires `SeAuditPrivilege`**, and **consuming (attaching to a ring) requires `SeSecurityPrivilege`**.

## 8.1.1 See also

- **[`<peios/msgpack.h>`](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md)** — building and parsing the payloads events carry.
- **[Auditing](/peios/security-fundamentals/auditing/overview.md)** — the operator-side view of the event and audit stream.

---

# 8.2 Emitting events

_Peios / Developing for Peios / SDK Reference / event.h — Events_

> Emitting a single event or a batch — the type string, the payload, and what the call validates.

```c
int peios_event_emit(const char *event_type, uint16_t event_type_len,
                     const void *payload, uint32_t payload_len);
```

Emits a single event. `event_type` is a **length-counted UTF-8** event kind such as `"my.app.login"` — *not* NUL-terminated, and its length must be non-zero. `payload` is `payload_len` bytes of MessagePack (one well-formed value). The kernel validates the payload (one well-formed MessagePack value within the configured size and nesting limits) and stamps `origin_class = userspace`. Returns `0`, or `-1` with `errno`:

| errno | Cause |
|---|---|
| `EPERM` | No `SeAuditPrivilege`. |
| `EINVAL` | Zero-length type, or a malformed payload. |
| `ENOSPC` | Payload exceeds the size caps. |
| `EAGAIN` | Rate-limited. |
| `EFAULT` | Bad pointer. |

Since the kernel's payload check matches [`peios_mp_validate`](/peios/developing-for-peios/sdk-reference/sdk-msgpack/msgpack-h-messagepack-codec.md#validator), you can validate in userspace first and turn a would-be `EINVAL` into a check you control.

```c
/* Build a payload, then emit. */
peios_mp_writer *w = peios_mp_writer_new();
peios_mp_write_map(w, 1);
peios_mp_write_str(w, "user", 4); peios_mp_write_str(w, "alice", 5);

const void *buf; ssize_t n = peios_mp_writer_bytes(w, &buf);
if (n >= 0)
    peios_event_emit("my.app.login", 12, buf, (uint32_t)n);
peios_mp_writer_free(w);
```

### 8.2.0.1 Batch emit

```c
struct peios_event_entry {
    const char *event_type;      /* length-counted UTF-8; not NUL-terminated */
    uint16_t    event_type_len;
    const void *payload;         /* MessagePack bytes */
    uint32_t    payload_len;
};

int peios_event_emit_batch(const struct peios_event_entry *entries,
                           uint32_t count, uint32_t *emitted_out);
```

`peios_event_emit_batch` emits several events in one call, **amortising the per-call overhead** — a single timestamp capture, identity capture, and consumer wake cover the whole batch. `count` is in `[1, KMES_BATCH_MAX_ENTRIES]`. It returns `0` if all `count` were emitted, or `-1` with the `errno` **of the first entry that failed**, with `*emitted_out` (if non-`NULL`) set to how many entries preceded the failure — so you know exactly where to resume. Rate-limiting is all-or-nothing here: an `EAGAIN` emits **none** of the batch.

---

# 8.3 Consuming events

_Peios / Developing for Peios / SDK Reference / event.h — Events_

> Attaching to a ring and reading events — the high-level reader, the low-level ring, and what a consumed event points into.

A consumed event is described by `struct peios_event`. The kernel-stamped header is copied to you by value; the two variable parts point into the ring mapping.

```c
struct peios_event {
    uint64_t timestamp;                  /* ns since the Unix epoch (CLOCK_REALTIME) */
    uint64_t sequence;                   /* per-CPU, per-boot monotonic (gap = lost events) */
    uint16_t cpu_id;
    uint8_t  origin_class;               /* 0 = userspace, 1 = KMES, 2 = KACS, 3 = LCS */
    uint8_t  effective_token_guid[16];
    uint8_t  true_token_guid[16];
    uint8_t  process_guid[16];
    const char *event_type;              /* not NUL-terminated; use event_type_len */
    uint16_t    event_type_len;
    const void *payload;                 /* a MessagePack value */
    uint32_t    payload_len;
};
```

The trusted metadata is the point of KMES: the `timestamp`, the identity GUIDs (the effective and true tokens, and the process), and the `origin_class` are stamped by the kernel and cannot be forged by the emitter. **`sequence` is per-CPU, per-boot monotonic — a gap in it means events were lost** (overwritten before you drained them).

> **Lifetime:** `event_type` and `payload` point into the ring mapping and are valid **only until the next read advance**, and only while the slot has not been overwritten. Copy out whatever you need before continuing to the next event.

### 8.3.0.1 Attaching to a ring

```c
int peios_event_attach(uint32_t cpu_id, uint64_t *capacity_out);
```

The low-level primitive: attach to CPU `cpu_id`'s ring buffer, returning a fd and writing the data-region capacity to `*capacity_out`. **Discover the CPU count** by counting up from `0` until `peios_event_attach` returns `-1` with `errno == EINVAL`. Requires `SeSecurityPrivilege` (`EPERM` otherwise). You then `mmap` the fd via [`peios_event_ring_map`](#low-level-ring). Most callers should use the high-level reader instead, which does the attach and mmap for you.

### 8.3.0.2 The high-level reader

```c
typedef struct peios_event_reader peios_event_reader;

peios_event_reader *peios_event_reader_open(uint32_t cpu_id);
void                peios_event_reader_close(peios_event_reader *r);
int      peios_event_reader_next(peios_event_reader *r, struct peios_event *out);
int      peios_event_reader_wait(peios_event_reader *r, int timeout_ms);
uint64_t peios_event_reader_lost(const peios_event_reader *r);
```

The reader owns the attach + mmap and hides the whole lock-free drain — memory barriers, lapping recovery, sequence-gap (lost-event) accounting, buffer resize/generation handling, and the futex wait. **You just loop `next`/`wait`.**

- `peios_event_reader_open` attaches to `cpu_id` and maps its ring, ready to drain (`NULL` with `errno` on failure). `peios_event_reader_close` tears it down.
- `peios_event_reader_next` fetches the next event into `out` (non-`NULL`). Returns **`1`** (event filled), **`0`** (none available right now — consider `wait`), or **`-1`** with `errno`. The `out` pointers are valid only until the next call.
- `peios_event_reader_wait` blocks until events are available or `timeout_ms` elapses (**negative = forever**). Returns `1` (call `next`), `0` (timeout/interrupted), or `-1`.
- `peios_event_reader_lost` returns the cumulative count of lost events (from sequence gaps) — poll it to monitor whether you're draining fast enough.

The canonical consume loop, per CPU:

```c
peios_event_reader *r = peios_event_reader_open(cpu);
for (;;) {
    struct peios_event ev;
    int rc = peios_event_reader_next(r, &ev);
    if (rc == 1) {
        /* handle ev — copy out event_type/payload before the next call */
    } else if (rc == 0) {
        peios_event_reader_wait(r, -1);   /* sleep until more arrive */
    } else {
        break;                            /* error */
    }
}
peios_event_reader_close(r);
```

To consume the whole machine, run one reader per CPU (discover the count as above), each typically on its own thread.

### 8.3.0.3 The low-level ring

For callers that want to drive the drain themselves — integrating the rings into a custom event loop, say — the ring API exposes the mapping directly. The accessors apply the correct memory barriers; **you** own the read position and the empty/lapping/generation checks.

```c
struct peios_event_ring { uint64_t _opaque[4]; };   /* opaque */

int  peios_event_ring_map(int fd, uint64_t capacity, struct peios_event_ring *ring);
void peios_event_ring_unmap(struct peios_event_ring *ring);

uint64_t peios_event_ring_capacity(const struct peios_event_ring *ring);
uint64_t peios_event_ring_write_pos(const struct peios_event_ring *ring);  /* acquire */
uint64_t peios_event_ring_tail_pos(const struct peios_event_ring *ring);   /* acquire */
uint64_t peios_event_ring_generation(const struct peios_event_ring *ring);
void     peios_event_ring_set_need_wake(const struct peios_event_ring *ring, int set);

ssize_t peios_event_ring_event_at(const struct peios_event_ring *ring,
                                  uint64_t read_pos, struct peios_event *out);
int     peios_event_ring_wait(const struct peios_event_ring *ring,
                              uint64_t read_pos, int timeout_ms);
```

- `peios_event_ring_map` maps and validates a ring fd from `peios_event_attach`; `ring` must be zeroed or previously unmapped (remapping an active ring fails `EBUSY`). `peios_event_ring_unmap` releases it.
- **Positions are free-running byte counters.** `write_pos` is where the producer will write next (acquire-loaded); `tail_pos` is the oldest still-live byte (advances as the ring laps); an event lives at `(read_pos & (capacity - 1))`. You drain by walking `read_pos` from `tail_pos` toward `write_pos`. `generation` changes when the buffer is resized — re-read `capacity` when it does.
- `peios_event_ring_event_at` parses the event at `read_pos` into `out` and returns its **byte size** (advance `read_pos` by that), or `-1` if the slot is corrupt. You must have confirmed `read_pos` is in `[tail_pos, write_pos)` first. Pass `out == NULL` to validate a slot and get its size without borrowing the `event_type`/`payload` pointers.
- Before sleeping, arm the advisory wake flag with `peios_event_ring_set_need_wake(ring, 1)`, then `peios_event_ring_wait` futex-waits until events past `read_pos` may be available or `timeout_ms` elapses (negative = forever): `1` (drain now), `0` (timeout/interrupted), `-1`.

The low-level loop mirrors the high-level one but with the position bookkeeping in your hands:

```c
uint64_t rp = peios_event_ring_tail_pos(&ring);
for (;;) {
    uint64_t wp = peios_event_ring_write_pos(&ring);
    while (rp < wp) {
        struct peios_event ev;
        ssize_t sz = peios_event_ring_event_at(&ring, rp, &ev);
        if (sz < 0) { /* corrupt slot — resync from tail_pos */ break; }
        /* handle ev */
        rp += (uint64_t)sz;
    }
    peios_event_ring_set_need_wake(&ring, 1);
    peios_event_ring_wait(&ring, rp, -1);
}
```

Reach for this only when the high-level reader's loop doesn't fit your event model; for almost everything, `peios_event_reader_*` is the right tool.

---

# 9.1 msgpack.h — MessagePack codec

_Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding_

> A general MessagePack codec whose reason for being is event payloads — its writer, reader and validator.

`<peios/msgpack.h>` is a small, self-contained [MessagePack](https://msgpack.org) codec. It exists because KMES event payloads *are* MessagePack: the kernel only *structurally validates* a payload on emit — it does not build or interpret it — so userspace owns the encode and decode. This codec is that path, and its validator's acceptance is deliberately **matched to the kernel's emit-time check**, so a payload this codec produces and validates is guaranteed to be accepted by [`peios_event_emit`](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md#emitting-events).

You can use it as a general MessagePack codec, but its reason for being is [events](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md).

It has three parts: a heap-backed **writer**, a stack-allocatable **reader**, and a **validator**.

## 9.1.1 See also

- **[`<peios/event.h>`](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md)** — the KMES events these payloads travel in.
- **[Library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md)** — the sticky-error builder model the writer follows.

---

# 9.2 Conventions

_Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding_

> The few rules that hold across the whole codec, and what they mean for buffers you pass in.

A few rules hold across the codec:

- **Integers are written in their smallest MessagePack form** automatically — you write an `int64`/`uint64` and the encoder picks the compact encoding.
- **`str` values must be valid UTF-8.** Use `bin` for arbitrary bytes. The reader enforces this on `str` reads too.
- **A valid payload is exactly one top-level value**, and an **empty buffer is not valid**. (A map or array at the top counts as that one value.)
- **The writer is sticky-error**, exactly like the [`<peios/security.h>` builders](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#memory-ownership): the write calls cannot fail individually; the first error latches and surfaces at `peios_mp_writer_bytes` / `peios_mp_writer_error`.

---

# 9.3 Writer

_Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding_

> Building a MessagePack value — scalars, containers, extensions and raw bytes, and taking the finished bytes.

```c
typedef struct peios_mp_writer peios_mp_writer;

peios_mp_writer *peios_mp_writer_new(void);
void             peios_mp_writer_free(peios_mp_writer *w);
void             peios_mp_writer_reset(peios_mp_writer *w);
```

Create a writer, append values, take the bytes, free it (or `reset` to reuse). All the append calls return `void` — errors latch.

### 9.3.0.1 Scalars

```c
void peios_mp_write_nil(peios_mp_writer *w);
void peios_mp_write_bool(peios_mp_writer *w, bool v);
void peios_mp_write_int(peios_mp_writer *w, int64_t v);
void peios_mp_write_uint(peios_mp_writer *w, uint64_t v);
void peios_mp_write_float(peios_mp_writer *w, double v);
void peios_mp_write_str(peios_mp_writer *w, const char *s, size_t len);  /* UTF-8 */
void peios_mp_write_bin(peios_mp_writer *w, const void *b, size_t len);
```

Use `peios_mp_write_int` for signed and `peios_mp_write_uint` for unsigned values; both are stored in the smallest form. `peios_mp_write_str` takes UTF-8 with an explicit length (no NUL needed); `peios_mp_write_bin` takes arbitrary bytes.

### 9.3.0.2 Containers

```c
void peios_mp_write_array(peios_mp_writer *w, uint32_t count);
void peios_mp_write_map(peios_mp_writer *w, uint32_t count);
```

Write the header, then exactly the promised number of values. **A map of `count` needs `2 * count` values** — `count` key/value *pairs* — written key, value, key, value…. An under- or over-filled container is not caught at the `write` call; it surfaces at `peios_mp_writer_bytes`, when the whole structure is validated.

```c
/* {"user": "alice", "ok": true} */
peios_mp_write_map(w, 2);
peios_mp_write_str(w, "user", 4);  peios_mp_write_str(w, "alice", 5);
peios_mp_write_str(w, "ok", 2);    peios_mp_write_bool(w, true);
```

### 9.3.0.3 Extensions and raw bytes

```c
void peios_mp_write_ext(peios_mp_writer *w, int8_t ext_type, const void *b, size_t len);
void peios_mp_write_raw(peios_mp_writer *w, const void *b, size_t len);
```

- `peios_mp_write_ext` writes a MessagePack extension value with a signed type id.
- `peios_mp_write_raw` appends **pre-encoded** MessagePack bytes verbatim — the escape hatch for splicing in a value you already have encoded. The result is still structurally validated as a whole at `peios_mp_writer_bytes`, so you can't smuggle malformed bytes through it.

### 9.3.0.4 Taking the bytes

```c
ssize_t peios_mp_writer_bytes(peios_mp_writer *w, const void **out);
int     peios_mp_writer_error(const peios_mp_writer *w);
```

`peios_mp_writer_bytes` **confirms the buffer is exactly one well-formed top-level value**, then borrows it: it writes a pointer to the encoded bytes through `out` (valid until the next mutating call on `w`) and returns the length. Pass `out == NULL` to validate and get the length without borrowing. It returns `-1` with `errno` — `EINVAL` on a latched error or a malformed/under-filled structure, `ENOMEM` on a prior allocation failure. `peios_mp_writer_error` returns the latched errno directly, or `0`.

Because this call validates, a successful `peios_mp_writer_bytes` is your guarantee the bytes are emit-ready.

---

# 9.4 Reader

_Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding_

> A stack-allocatable cursor over a borrowed buffer — peeking at the next value, then reading scalars, strings and containers.

The reader is a **cursor over a borrowed buffer** — stack-allocatable, no heap, no free. It decodes one value at a time, advancing the cursor.

```c
struct peios_mp_reader { uint64_t _opaque[4]; };   /* opaque — do not inspect */

void   peios_mp_reader_init(struct peios_mp_reader *r, const void *buf, size_t len);
size_t peios_mp_reader_remaining(const struct peios_mp_reader *r);
```

Declare a `struct peios_mp_reader` locally and `peios_mp_reader_init` it over your buffer before use. `buf` may be `NULL` only when `len` is zero. Borrowed `str`/`bin`/`ext` pointers the reader hands back point **into the original buffer** and are valid for as long as it lives. `peios_mp_reader_remaining` reports the unconsumed byte count.

### 9.4.0.1 Peeking

```c
enum peios_mp_type {
    PEIOS_MP_NIL, PEIOS_MP_BOOL, PEIOS_MP_INT, PEIOS_MP_FLOAT,
    PEIOS_MP_STR, PEIOS_MP_BIN, PEIOS_MP_ARRAY, PEIOS_MP_MAP, PEIOS_MP_EXT,
};

int peios_mp_peek(const struct peios_mp_reader *r);
```

`peios_mp_peek` returns the `peios_mp_type` of the next value **without consuming it**, or `-1` at end-of-input or on an invalid lead byte. Note that integers of every width and sign report as `PEIOS_MP_INT` — read them with `peios_mp_read_int` or `peios_mp_read_uint` as you prefer. Peek is how you drive a dispatch over a value whose type you don't know ahead of time.

### 9.4.0.2 Reading scalars

```c
int peios_mp_read_nil(struct peios_mp_reader *r);
int peios_mp_read_bool(struct peios_mp_reader *r, bool *out);
int peios_mp_read_int(struct peios_mp_reader *r, int64_t *out);
int peios_mp_read_uint(struct peios_mp_reader *r, uint64_t *out);
int peios_mp_read_float(struct peios_mp_reader *r, double *out);
```

Each **consumes one value on success** (returns `0`) and leaves the cursor **untouched on a type mismatch or truncation** (`-1` with `errno == EINVAL`) — so a failed read is safe to follow with a different-typed read or a `peek`. The `out` pointer is optional: pass `NULL` to consume/type-check a value without receiving its payload.

### 9.4.0.3 Reading strings, bytes, containers, extensions

```c
ssize_t peios_mp_read_str(struct peios_mp_reader *r, const char **out);
ssize_t peios_mp_read_bin(struct peios_mp_reader *r, const void **out);
ssize_t peios_mp_read_array(struct peios_mp_reader *r);
ssize_t peios_mp_read_map(struct peios_mp_reader *r);
ssize_t peios_mp_read_ext(struct peios_mp_reader *r, int8_t *type_out, const void **out);
int     peios_mp_skip(struct peios_mp_reader *r);
```

- `peios_mp_read_str` / `peios_mp_read_bin` **borrow** the bytes (a pointer into the reader's buffer via `out`) and return the length, or `-1`. Strings are **not** NUL-terminated — use the length — and `peios_mp_read_str` rejects invalid UTF-8.
- `peios_mp_read_array` returns the **element count**; `peios_mp_read_map` returns the **key/value pair count** (so read `2 * count` values). After the header you read that many values yourself.
- `peios_mp_read_ext` borrows an extension value's bytes, reporting its signed type id through `type_out` (both `type_out` and `out` are independently optional), and returns the data length.
- `peios_mp_skip` consumes **exactly one complete value**, descending into nested containers — the way to ignore a value (or a whole subtree) you don't care about. `0` / `-1`.

```c
struct peios_mp_reader r;
peios_mp_reader_init(&r, payload, payload_len);

ssize_t pairs = peios_mp_read_map(&r);          /* top-level map */
for (ssize_t i = 0; i < pairs; i++) {
    const char *key; ssize_t klen = peios_mp_read_str(&r, &key);
    /* dispatch on key… then read or skip the value */
    peios_mp_skip(&r);
}
```

---

# 9.5 Validator

_Peios / Developing for Peios / SDK Reference / msgpack.h — Encoding_

> Checking a buffer in one pass, with acceptance that matches the kernel's own emit-time check.

```c
int peios_mp_validate(const void *buf, size_t len, uint32_t max_depth);
```

`peios_mp_validate` confirms `buf`/`len` is **exactly one well-formed MessagePack value**: UTF-8 strings, nesting bounded by `max_depth`, no trailing bytes, non-empty. Returns `0` if valid, `-1` with `errno == EINVAL` otherwise.

Crucially, its acceptance **matches the kernel's emit-time check**, so a `0` return means the [event emit calls](/peios/developing-for-peios/sdk-reference/sdk-events-api/event-h-events-kmes.md#emitting-events) will accept the payload — *at this depth bound*. Pass `KMES_CONFIG_MAX_NESTING_DEPTH_DEFAULT` (32) for the default emit limit; the top-level value is depth 1. Validate before emitting when a payload comes from an untrusted or dynamic source, so you turn a would-be `EINVAL` from the kernel into a check you control.

---

# 10.1 rsi/source.h — Becoming a source

_Peios / Developing for Peios / SDK Reference / rsi/source.h — Becoming a Source_

> The one job of this header — declaring which hives your process backs, registering with the kernel, and getting a source fd.

`<rsi/source.h>` is where a registry **source** begins. A source is a storage backend for the [LCS registry](/peios/developing-for-peios/sdk-registry/overview.md) — the provider counterpart to libpeios's registry *client*. Where a client opens keys and reads values, a source is what actually *holds* those keys and values and answers the kernel's requests for them.

This header has one job: **registration**. You declare which hives your process backs, register with the kernel, and get back a **source fd**. From that point on you serve the RSI (Registry Source Interface) protocol on that fd — [reading requests](/peios/developing-for-peios/sdk-reference/sdk-rsi-request/rsi-request-h-decoding-requests.md) and [writing responses](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md). Registration requires `SeTcbPrivilege`. The RSI wire constants (`RSI_HIVE_PRIVATE`, `RSI_*`) come from `<pkm/lcs.h>`.

This is part of **librsi**, a separate library from libpeios — link `-lrsi` and include `<rsi.h>` (or the individual `<rsi/*.h>`). It follows the same [library conventions](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md): raw fds, `int` returning `0`/`-1`+errno, and the errno passed straight through from the kernel.

## 10.1.1 See also

- **[Registry sources overview](/peios/developing-for-peios/registry-sources/overview.md)** — what a source is and how the RSI protocol flows.
- **[The registry](/peios/using-peios/registry-concepts/overview.md)** — the operator-side model of hives, layers, and sources.

---

# 10.2 Describing a hive

_Peios / Developing for Peios / SDK Reference / rsi/source.h — Becoming a Source_

> The rsi_hive struct a source fills in per hive it backs, field by field.

A **hive** is a subtree of the registry with its own root key. A source declares one `struct rsi_hive` per hive it backs:

```c
struct rsi_hive {
    const void *name;         /* hive name (not NUL-terminated) */
    uint32_t    name_len;
    uint32_t    flags;        /* RSI_HIVE_PRIVATE, or 0 for a global hive */
    uint8_t     root_guid[16];/* root key GUID */
    uint8_t     scope_guid[16];/* private hives; zero for a global hive */
};
```

| Field | Meaning |
|---|---|
| `name` / `name_len` | The hive's name, length-counted (not NUL-terminated). |
| `flags` | `RSI_HIVE_PRIVATE` for a private (scoped) hive, or `0` for a global one. |
| `root_guid` | The GUID of the hive's root key — the anchor every path in the hive resolves from. |
| `scope_guid` | For a **private** hive, the scope GUID that bounds who can resolve it; **zero for a global hive**. |

A **global** hive is visible system-wide; a **private** hive is scoped by `scope_guid` and resolvable only by tokens holding that scope (see the token [LCS credentials](/peios/developing-for-peios/sdk-reference/sdk-tokens/token-h-tokens-and-sessions.md#lcs-registry-credentials)). Set `RSI_HIVE_PRIVATE` and a non-zero `scope_guid` together for a private hive; leave both clear for a global one.

---

# 10.3 Registering

_Peios / Developing for Peios / SDK Reference / rsi/source.h — Becoming a Source_

> rsi_register opens the registry device and registers every declared hive, returning the source fd the serve loop runs on.

```c
int rsi_register(const struct rsi_hive *hives, uint32_t count, uint64_t max_sequence);
```

Opens `/dev/pkm_registry` and registers all `count` hives in one call, returning the **source fd** — the descriptor you then `read(2)` requests and `write(2)` responses on — or `-1` with `errno`.

| Argument | Meaning |
|---|---|
| `hives` / `count` | The hives this source serves. `count` must be `>= 1`; the kernel enforces its configured `MaxHivesPerSource` limit. |
| `max_sequence` | The highest sequence number this source has **already persisted**. The kernel resumes its global sequence counter *past* this value, so a source that has durable state from a previous run must report it here to avoid reusing sequence numbers. A fresh source with no persisted state passes `0`. |

Errors include `EPERM` (no `SeTcbPrivilege`), `EINVAL`, `ENOSPC` (over the hive limit), `ENOMEM`, `EFAULT`, and any error from the underlying `/dev/pkm_registry` `open(2)`.

```c
struct rsi_hive hive = {
    .name = "MyStore", .name_len = 7,
    .flags = 0,                                  /* global hive */
    .root_guid = { /* … 16 bytes … */ },
};

int src = rsi_register(&hive, 1, /*max_sequence=*/0);
if (src < 0) { perror("rsi_register"); return -1; }
/* `src` is now the source fd — serve the RSI protocol on it. */
```

The `max_sequence` parameter is the one piece of state a durable source must get right: on restart, scan your persisted data for the highest sequence you ever wrote and pass it, so the kernel never hands out a sequence number you've already used.

---

# 10.4 What comes next

_Peios / Developing for Peios / SDK Reference / rsi/source.h — Becoming a Source_

> Registration is the whole of this header — where the serve loop and the response helpers live.

Registration is the whole of this header. Once you hold the source fd, the serve loop lives in the other two:

- **[`<rsi/request.h>`](/peios/developing-for-peios/sdk-reference/sdk-rsi-request/rsi-request-h-decoding-requests.md)** — read and decode the requests the kernel sends.
- **[`<rsi/response.h>`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md)** — build and send the replies.

The [serving requests](/peios/developing-for-peios/registry-sources/serving-requests.md) guide ties them together into a working serve loop.

---

# 11.1 rsi/request.h — Decoding requests

_Peios / Developing for Peios / SDK Reference / rsi/request.h — Decoding Requests_

> The shape of every serve loop — read a frame, parse the header, dispatch on the op-code, decode with the matching parser.

`<rsi/request.h>` is the receiving half of a registry source's serve loop. The kernel sends your source [RSI](/peios/developing-for-peios/registry-sources/overview.md) requests — "look up this child", "store this value", "begin this transaction" — as framed messages on the source fd. This header reads one frame, splits its header from its payload, and decodes the payload into a flat, typed struct you can act on.

The shape of the loop is always: **read a frame → parse the header → dispatch on the op-code → decode the payload with the matching parser**. The decoders are thin wrappers over the kernel's own RSI parsers, so your wire handling is guaranteed compatible with what the kernel sent.

> **Borrowing:** every decoded name/data field is a `(ptr, len)` pair that **borrows into your frame buffer**. The pointers are valid only until you reuse that buffer for the next `rsi_read_request`. Copy out anything you need to keep across iterations. This is the same [borrow discipline](/peios/developing-for-peios/sdk-reference/sdk-conventions/library-conventions.md#memory-ownership) as libpeios's views.

Op-code and field constants (`RSI_LOOKUP`, `RSI_WRITE_KEY_FIELD_*`, `RSI_TXN_*`) come from `<pkm/lcs.h>`.

## 11.1.1 See also

- **[`<rsi/response.h>`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md)** — building the reply each op expects.
- **[Serving requests](/peios/developing-for-peios/registry-sources/serving-requests.md)** — the read/parse/dispatch/respond loop in full.

---

# 11.2 Reading and parsing a frame

_Peios / Developing for Peios / SDK Reference / rsi/request.h — Decoding Requests_

> The rsi_request struct, what each field means, and how to read one frame off the source fd and parse its header.

```c
struct rsi_request {
    uint64_t    request_id;   /* echo this in the response */
    uint64_t    txn_id;       /* transaction id (0 outside a transaction) */
    const void *payload;      /* borrowed; valid until the frame is reused */
    uint32_t    payload_len;
    uint16_t    op_code;      /* RSI_LOOKUP, RSI_SET_VALUE, … — dispatch on this */
};

ssize_t rsi_read_request(int fd, void *buf, size_t cap);
int     rsi_parse_request(const void *frame, size_t len, struct rsi_request *out);
```

- **`rsi_read_request`** reads one framed request from the source fd into `buf` — a thin `read(2)` wrapper that **blocks** until a request is queued, then returns the frame length (pass it to `rsi_parse_request`). It returns **`0` at EOF** (the source is closing — leave the loop) or `-1` with `errno`, notably **`EMSGSIZE`** if `cap` is smaller than the pending frame (size `buf` generously, or grow and retry).
- **`rsi_parse_request`** splits a frame into its header and payload view, filling `out` with the `request_id` (which you must echo in the response), the `txn_id` (`0` when the request is not inside a transaction), the `op_code` to dispatch on, and a **borrowed** `payload` pointer. Returns `0`, or `-1` with `errno` (`EINVAL` on NULL args, `EBADMSG` on a malformed frame).

---

# 11.3 The decoders

_Peios / Developing for Peios / SDK Reference / rsi/request.h — Decoding Requests_

> One decoder per operation, each filling a flat struct with GUIDs by value and names as borrowed pointers.

Each decoder takes the parsed `req` and fills a flat struct: GUIDs by value, names and data as borrowed `(ptr, len)` pairs. All return `0`, or `-1` with `errno` — **`EINVAL`** if the arguments are NULL *or the decoder doesn't match `req->op_code`* (so calling the wrong decoder for an op is a clean error), and **`EBADMSG`** on a malformed payload. You dispatch on `req.op_code` and call the matching one.

### 11.3.0.1 Path and entry operations

These operate on the name→GUID bindings that make up the key hierarchy. A *child* is named under a *parent* GUID, and entries live in *layers*.

```c
/* LOOKUP — is child_name visible under parent_guid? */
struct rsi_lookup {
    uint8_t     parent_guid[16];
    const void *child_name;  uint32_t child_name_len;
};
int rsi_request_lookup(const struct rsi_request *req, struct rsi_lookup *out);

/* CREATE_ENTRY — bind child_name → child_guid in layer_name. */
struct rsi_create_entry {
    uint8_t     parent_guid[16];
    uint8_t     child_guid[16];
    const void *child_name;  uint32_t child_name_len;
    const void *layer_name;  uint32_t layer_name_len;
    uint64_t    sequence;
};
int rsi_request_create_entry(const struct rsi_request *req, struct rsi_create_entry *out);

/* HIDE_ENTRY — tombstone child_name in layer_name. */
struct rsi_hide_entry {
    uint8_t     parent_guid[16];
    const void *child_name;  uint32_t child_name_len;
    const void *layer_name;  uint32_t layer_name_len;
    uint64_t    sequence;
};
int rsi_request_hide_entry(const struct rsi_request *req, struct rsi_hide_entry *out);

/* DELETE_ENTRY — remove child_name's entry in layer_name. */
struct rsi_delete_entry {
    uint8_t     parent_guid[16];
    const void *child_name;  uint32_t child_name_len;
    const void *layer_name;  uint32_t layer_name_len;
};
int rsi_request_delete_entry(const struct rsi_request *req, struct rsi_delete_entry *out);

/* ENUM_CHILDREN — list the children of parent_guid. */
struct rsi_enum_children { uint8_t parent_guid[16]; };
int rsi_request_enum_children(const struct rsi_request *req, struct rsi_enum_children *out);
```

| Op | You must | Reply with |
|---|---|---|
| `LOOKUP` | Resolve `child_name` under `parent_guid` across your layers. | [`rsi_respond_lookup`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md#lookup) |
| `CREATE_ENTRY` | Bind `child_name` → `child_guid` in `layer_name` at `sequence`. | status |
| `HIDE_ENTRY` | Place a tombstone for `child_name` in `layer_name`. | status |
| `DELETE_ENTRY` | Remove `child_name`'s entry in `layer_name`. | status |
| `ENUM_CHILDREN` | List every child of `parent_guid`. | [`rsi_respond_enum_children`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md#enum_children) |

### 11.3.0.2 Key operations

These operate on key *metadata* records — the non-layered facts about a key (its name, parent, security descriptor, flags).

```c
/* CREATE_KEY — create the metadata record guid under parent_guid. */
struct rsi_create_key {
    uint8_t     guid[16];
    uint8_t     parent_guid[16];
    const void *name;  uint32_t name_len;
    const void *sd;    uint32_t sd_len;
    uint8_t     volatile_key;  /* 1 if volatile */
    uint8_t     symlink;       /* 1 if a symlink */
};
int rsi_request_create_key(const struct rsi_request *req, struct rsi_create_key *out);

/* READ_KEY / DROP_KEY — a request carrying just a key GUID. */
struct rsi_key_guid { uint8_t guid[16]; };
int rsi_request_read_key(const struct rsi_request *req, struct rsi_key_guid *out);
int rsi_request_drop_key(const struct rsi_request *req, struct rsi_key_guid *out);

/* WRITE_KEY — update the mutable fields of guid named by field_mask. */
struct rsi_write_key {
    uint8_t     guid[16];
    uint32_t    field_mask;        /* RSI_WRITE_KEY_FIELD_SD | …_LAST_WRITE_TIME */
    const void *sd;  uint32_t sd_len;/* NULL when the SD bit is clear */
    uint64_t    last_write_time;   /* valid only when the time bit is set */
};
int rsi_request_write_key(const struct rsi_request *req, struct rsi_write_key *out);
```

| Op | You must | Reply with |
|---|---|---|
| `CREATE_KEY` | Store the metadata record for `guid` (its name, parent, `sd`, and the `volatile_key`/`symlink` flags). | status |
| `READ_KEY` | Return the metadata of `guid`. | [`rsi_respond_read_key`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md#read_key) |
| `DROP_KEY` | Delete the metadata record for `guid`. | status |
| `WRITE_KEY` | Update **only** the fields selected in `field_mask` — the SD when `RSI_WRITE_KEY_FIELD_SD` is set, the `last_write_time` when its bit is set — leaving the rest untouched. | status |

`WRITE_KEY`'s `field_mask` is the important detail: `sd` is `NULL` unless the SD bit is set, and `last_write_time` is meaningful only when the time bit is set, so consult the mask before reading either.

### 11.3.0.3 Value operations

These operate on the typed values stored on a key, each written into a layer.

```c
/* QUERY_VALUES — read value_name (or all values when query_all) of guid. */
struct rsi_query_values {
    uint8_t     guid[16];
    const void *value_name;  uint32_t value_name_len;
    uint8_t     query_all;   /* 1 = every value (then value_name is ignored) */
};
int rsi_request_query_values(const struct rsi_request *req, struct rsi_query_values *out);

/* SET_VALUE — store value_name in layer_name with the given type/data. */
struct rsi_set_value {
    uint8_t     guid[16];
    const void *value_name;  uint32_t value_name_len;
    const void *layer_name;  uint32_t layer_name_len;
    uint32_t    value_type;
    const void *data;  uint32_t data_len;
    uint64_t    sequence;
    uint64_t    expected_sequence;  /* CAS guard (0 disables) */
};
int rsi_request_set_value(const struct rsi_request *req, struct rsi_set_value *out);

/* DELETE_VALUE_ENTRY — remove value_name's entry in layer_name. */
struct rsi_delete_value_entry {
    uint8_t     guid[16];
    const void *value_name;  uint32_t value_name_len;
    const void *layer_name;  uint32_t layer_name_len;
};
int rsi_request_delete_value_entry(const struct rsi_request *req,
                                   struct rsi_delete_value_entry *out);

/* SET_BLANKET_TOMBSTONE — set or clear a blanket tombstone on layer_name. */
struct rsi_set_blanket_tombstone {
    uint8_t     guid[16];
    const void *layer_name;  uint32_t layer_name_len;
    uint8_t     set;         /* 1 = set, 0 = clear */
    uint64_t    sequence;
};
int rsi_request_set_blanket_tombstone(const struct rsi_request *req,
                                      struct rsi_set_blanket_tombstone *out);
```

| Op | You must | Reply with |
|---|---|---|
| `QUERY_VALUES` | Return `value_name` — or every value when `query_all` is `1` (then `value_name` is ignored) — plus any blanket tombstones. | [`rsi_respond_query_values`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md#query_values) |
| `SET_VALUE` | Store `value_name` of `value_type` in `layer_name`. Honour `expected_sequence` as a compare-and-swap guard (`0` disables it) — reject with a non-OK status if the current sequence differs. | status |
| `DELETE_VALUE_ENTRY` | Remove `value_name`'s entry in `layer_name`. | status |
| `SET_BLANKET_TOMBSTONE` | Set (`set == 1`) or clear a blanket tombstone on `layer_name`, masking all lower values at once. | status |

### 11.3.0.4 Transaction operations

The kernel drives transaction boundaries; your source honours them so a group of writes commits or aborts atomically.

```c
/* BEGIN_TRANSACTION — open transaction_id in mode. */
struct rsi_begin_transaction {
    uint64_t    transaction_id;
    uint32_t    mode;   /* RSI_TXN_READ_WRITE (0) or RSI_TXN_READ_ONLY (1) */
};
int rsi_request_begin_transaction(const struct rsi_request *req,
                                  struct rsi_begin_transaction *out);

/* COMMIT_TRANSACTION / ABORT_TRANSACTION — a request carrying just a transaction id. */
struct rsi_transaction { uint64_t transaction_id; };
int rsi_request_commit_transaction(const struct rsi_request *req, struct rsi_transaction *out);
int rsi_request_abort_transaction(const struct rsi_request *req, struct rsi_transaction *out);
```

| Op | You must | Reply with |
|---|---|---|
| `BEGIN_TRANSACTION` | Open `transaction_id` in `mode` (`RSI_TXN_READ_WRITE` or `RSI_TXN_READ_ONLY`); buffer subsequent writes tagged with this id. | status |
| `COMMIT_TRANSACTION` | Atomically apply everything buffered under `transaction_id`. | status |
| `ABORT_TRANSACTION` | Discard everything buffered under `transaction_id`. | status |

Requests that belong to a transaction carry its id in `req.txn_id`; a `txn_id` of `0` means the request is outside any transaction.

### 11.3.0.5 Layer operations

```c
/* DELETE_LAYER / FLUSH — a request carrying just a length-prefixed name. */
struct rsi_name { const void *name;  uint32_t name_len; };
int rsi_request_delete_layer(const struct rsi_request *req, struct rsi_name *out);
int rsi_request_flush(const struct rsi_request *req, struct rsi_name *out);
```

| Op | You must | Reply with |
|---|---|---|
| `DELETE_LAYER` | Remove the entire named layer, reporting the GUIDs of any keys it orphaned. | [`rsi_respond_delete_layer`](/peios/developing-for-peios/sdk-reference/sdk-rsi-response/rsi-response-h-building-responses.md#delete_layer) |
| `FLUSH` | Durably persist pending writes for the named hive, replying only once persistence is confirmed. | status |

---

# 12.1 rsi/response.h — Building responses

_Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses_

> Echoing the request id and op-code with an RSI status — the response helpers, and why most operations are status-only.

`<rsi/response.h>` is the sending half of a source's serve loop. After you handle a request, you reply on the source fd with a framed response. This header builds those frames for you: you pass the result as flat arrays, and librsi validates and heap-encodes the wire frame — you never hand-pack a byte.

Every response echoes the request's id and its op-code (OR'd with the response bit) and carries an `RSI_*` status. **Most operations are status-only**; five carry a payload on success. Any operation can report a *non-OK* status with the status-only helper.

For the wire, a response is a 14-byte header (echoed request id, op-code | `RSI_RESPONSE_BIT`) plus a 4-byte `RSI_*` status, followed by an op-specific payload for payload-bearing successes; multi-byte integers are little-endian and names/data are length-prefixed. You don't assemble any of that — the helpers do. Status and target-type constants (`RSI_OK`, `RSI_PATH_TARGET_GUID`, …) come from `<pkm/lcs.h>`.

## 12.1.1 See also

- **[`<rsi/request.h>`](/peios/developing-for-peios/sdk-reference/sdk-rsi-request/rsi-request-h-decoding-requests.md)** — decoding the request each of these replies to.
- **[Building responses](/peios/developing-for-peios/registry-sources/building-responses.md)** — choosing and filling the right responder.

---

# 12.2 Status codes

_Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses_

> The statuses a response may carry and the errno each becomes for the registry client, so the right one matters.

Every response carries exactly one of these statuses. The kernel translates a non-OK status into the errno the registry client sees, so send the code that matches what actually happened:

| Code | When to send it |
|---|---|
| `RSI_OK` | The operation succeeded. Status-only ops report it via `rsi_respond_status`; the five payload-bearing ops must use their own helper. |
| `RSI_NOT_FOUND` | The requested key, entry, value, or layer does not exist in your store (client sees `ENOENT`). |
| `RSI_ALREADY_EXISTS` | A create collided with something that already exists (client sees `EEXIST`). |
| `RSI_STORAGE_ERROR` | Your backing store failed — I/O error, corruption, anything the client can't fix (client sees `EIO`). |
| `RSI_NOT_EMPTY` | The operation needs the key to have no children, and it has some (client sees `ENOTEMPTY`). |
| `RSI_TOO_LARGE` | The data exceeds what the source is willing or able to store (client sees `ENOSPC`). |
| `RSI_TXN_BUSY` | A transaction can't proceed right now — e.g. write-lock contention; the operation may be retried (client sees `EBUSY`). |
| `RSI_INVALID` | The request is well-formed RSI but violates the source's rules or refers to something malformed (client sees `EINVAL`). |
| `RSI_CAS_FAILED` | A sequence-guarded write's `expected_sequence` did not match the current entry — the compare-and-swap lost (client sees `EAGAIN` and retries). |
| `RSI_TXN_NOT_SUPPORTED` | Reply to `BEGIN_TRANSACTION` from a source that does not implement transactions (client sees `ENOTSUP`). |

---

# 12.3 The response contract

_Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses_

> The rules every response helper enforces, and what violating one costs you.

All `rsi_respond_*` helpers return `0`, or `-1` with `errno`. A set of rules applies to **every** helper, and violating one is an `EINVAL` caller-contract error:

- **`(ptr, len)` pairs:** a pointer may be `NULL` only when its length/count is zero.
- **Boolean fields** (`volatile_key`, `symlink`, target types) must be exactly `0` or `1`.
- **Hidden path targets** (`RSI_PATH_TARGET_HIDDEN`) must carry an **all-zero** `target_guid`.
- **`LOOKUP`/`ENUM_CHILDREN` metadata** must exactly cover the GUID path targets referenced — no missing metadata, no duplicates, no unreferenced entries.
- **`DELETE_LAYER` orphan GUIDs** must be nonzero and unique.

Beyond `EINVAL`, any helper can also fail with `ENOMEM` (during validation or frame allocation), `EOVERFLOW` (validation arithmetic or the assembled frame too large), `EIO` (a short `write`), or the raw `write(2)` errno. Per-helper `EINVAL` additions are noted below.

---

# 12.4 Sending a pre-built frame

_Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses_

> Writing an already-built response frame to the source fd, for a source that encodes its own.

```c
ssize_t rsi_write_response(int fd, const void *frame, size_t len);
```

Writes one already-built response frame to the source fd — a thin `write(2)` wrapper returning the bytes written, or `-1` with `errno`. Most callers never need this; the `rsi_respond_*` helpers build *and* send. It exists for callers assembling frames by other means.

---

# 12.5 Status-only responses

_Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses_

> The workhorse helper — what it is for, and the cases where it is the whole of a response.

```c
int rsi_respond_status(int fd, const struct rsi_request *req, uint32_t status);
```

The workhorse. Use it for:

- **status-only ops on success** — pass `status = RSI_OK`; and
- **any op reporting a non-OK status** — a `LOOKUP` that found nothing, a `SET_VALUE` that failed a compare-and-swap, a permission error: reply with the appropriate `RSI_*` status here, whatever the op.

It fails with `EINVAL` on a bad `req`, an unknown `status`, or `RSI_OK` given for a payload-bearing op (those must use their own helper on success), plus `EIO` / the `write` error.

The rule of thumb: **on failure, always `rsi_respond_status`; on success, `rsi_respond_status` unless the op is one of the five below.**

---

# 12.6 Payload-bearing responses

_Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses_

> The five operations that return data on success, and the flat arrays each helper takes to encode the frame for you.

Five operations return data on success. Each takes the result as flat arrays and encodes the frame for you.

### 12.6.0.1 LOOKUP

```c
struct rsi_path_entry {
    const void *layer;  uint32_t layer_len;
    uint8_t     target_type;   /* RSI_PATH_TARGET_GUID (0) / RSI_PATH_TARGET_HIDDEN (1) */
    uint8_t     target_guid[16];
    uint64_t    sequence;
};
struct rsi_key_metadata {
    uint8_t     guid[16];
    const void *sd;  uint32_t sd_len;
    uint8_t     volatile_key;
    uint8_t     symlink;
    uint64_t    last_write_time;
};

int rsi_respond_lookup(int fd, const struct rsi_request *req,
                       const struct rsi_path_entry *entries, uint32_t entry_count,
                       const struct rsi_key_metadata *metadata, uint32_t metadata_count);
```

Answers a `LOOKUP` with the resolved path `entries` for the child — one per layer that has a view of it, each either a **GUID** target or a **HIDDEN** (tombstone) target — plus the `metadata` for every key the entries reference. A `RSI_PATH_TARGET_HIDDEN` entry must carry an all-zero `target_guid`; the metadata must exactly cover the GUID targets. `EINVAL` if `req` is not a `LOOKUP`, a nonzero count has a NULL array, or an entry has invalid target/boolean fields, missing/duplicate metadata, or unreferenced metadata.

### 12.6.0.2 ENUM_CHILDREN

```c
struct rsi_child_entry {
    const void            *child_name;  uint32_t child_name_len;
    const struct rsi_path_entry *entries;  uint32_t entry_count;
};

int rsi_respond_enum_children(int fd, const struct rsi_request *req,
                              const struct rsi_child_entry *children, uint32_t child_count,
                              const struct rsi_key_metadata *metadata, uint32_t metadata_count);
```

Answers an `ENUM_CHILDREN` with each `child` — its name and the [path entries](#lookup) that resolve it — plus the `metadata` for every referenced key. The same target/boolean/metadata-coverage rules as `LOOKUP` apply. `EINVAL` on the same conditions, scoped to `ENUM_CHILDREN`.

### 12.6.0.3 READ_KEY

```c
int rsi_respond_read_key(int fd, const struct rsi_request *req, const void *name,
                         uint32_t name_len, const uint8_t *parent_guid, const void *sd,
                         uint32_t sd_len, uint8_t volatile_key, uint8_t symlink,
                         uint64_t last_write_time);
```

Answers a `READ_KEY` with the key's non-layered metadata: its `name`, `parent_guid`, security descriptor (`sd`), the `volatile_key`/`symlink` flags, and `last_write_time`. `EINVAL` if `req` is not a `READ_KEY`, `parent_guid` is NULL, or a boolean field is invalid.

### 12.6.0.4 QUERY_VALUES

```c
struct rsi_value_entry {
    const void *value_name;  uint32_t value_name_len;
    const void *layer_name;  uint32_t layer_name_len;
    uint32_t    value_type;
    const void *data;  uint32_t data_len;
    uint64_t    sequence;
};
struct rsi_blanket_entry {
    const void *layer_name;  uint32_t layer_name_len;
    uint64_t    sequence;
};

int rsi_respond_query_values(int fd, const struct rsi_request *req,
                             const struct rsi_value_entry *entries, uint32_t entry_count,
                             const struct rsi_blanket_entry *blankets, uint32_t blanket_count);
```

Answers a `QUERY_VALUES` with the value `entries` — each value's name, the layer it lives in, its type, data, and sequence — plus the `blankets` (the blanket tombstones on this key, each a layer and sequence). The kernel resolves precedence across the layers you report. `EINVAL` if `req` is not a `QUERY_VALUES` or a nonzero count has a NULL array.

### 12.6.0.5 DELETE_LAYER

```c
int rsi_respond_delete_layer(int fd, const struct rsi_request *req,
                             const uint8_t *orphaned_guids, uint32_t orphaned_count);
```

Answers a `DELETE_LAYER` with the GUIDs of the keys the deleted layer orphaned — a flat `orphaned_count * 16`-byte array. The GUIDs must be nonzero and unique. `EINVAL` if `req` is not a `DELETE_LAYER` or a nonzero count has a NULL array, a nil GUID, or a duplicate.

---

# 12.7 The five at a glance

_Peios / Developing for Peios / SDK Reference / rsi/response.h — Building Responses_

> A one-page summary of the payload-bearing responses, and the rule covering everything else.

| Success response | Op | Payload |
|---|---|---|
| `rsi_respond_lookup` | `LOOKUP` | path entries + referenced key metadata |
| `rsi_respond_enum_children` | `ENUM_CHILDREN` | children (name + path entries) + metadata |
| `rsi_respond_read_key` | `READ_KEY` | one key's non-layered metadata |
| `rsi_respond_query_values` | `QUERY_VALUES` | value entries + blanket tombstones |
| `rsi_respond_delete_layer` | `DELETE_LAYER` | orphaned key GUIDs |

Every other op — and every failure of these — is `rsi_respond_status`.

---

# What DWE is

_Peios / Developing for Peios / DWE_

> Developer Workflow Embeddings — development paths built into core Peios software. Its first component, dwed, is an unauthenticated SYSTEM control surface that lets you drive a running machine from outside, across as long as an investigation takes.

**Developer Workflow Embeddings** are dedicated development paths built into core Peios software: the tools carry first-class support for developing against them, rather than being poked at from the outside by whatever a developer can improvise.

Its first and most general component is **`dwed`**, a service that gives you a persistent, maximally-privileged way to talk to a machine that is already running. Everything below is about that.

> [!CAUTION]
> `dwed` performs **no authentication of any kind**. Anything that can reach its socket owns that machine as `SYSTEM`. It exists only on a dedicated development ISO and must never be present on a machine you would not hand to a stranger.
>
> This is the design, not a gap to be closed later. See [The security posture](#the-security-posture).

## The problem it solves

Debugging a live system means asking it one question, reading the answer, and asking a better one. The loop is only as good as how quickly you can go round it.

Without something like `dwed`, driving a Peios machine from outside means a serial console: boot the machine, feed it a script, read what comes back, and start again. Three things about that hurt more than they look:

- **Nothing survives.** Each run is a fresh boot, so every question has to be planned in advance and packed into one script. A question that occurs to you halfway through the output cannot be asked without starting over.
- **The output is one stream.** A console interleaves what you typed, what the shell echoed, and what the program wrote to `stdout` and `stderr` — with no reliable way to pull them apart afterwards. Exit codes are not carried at all unless the script prints them itself.
- **You cannot be `SYSTEM`.** A console session is a logon session belonging to a person. The most privileged thing a machine has to offer is not reachable through it at all.

`dwed` exists because the machine is already persistent. What was missing was anything willing to talk to it in between questions.

## What it gives you

A socket into a running machine that answers **structured** requests as `SYSTEM`:

- `stdout` and `stderr` come back **separately**, as raw bytes, with a real exit status.
- Commands run **directly**, not through a shell, so there is no quoting layer between what you meant and what ran.
- Work can be **detached** — started now, collected several connections later. The job outlives the connection that started it, which is what makes an investigation spanning hours possible.
- Files move in and out **whole and binary-safe**, rather than through `base64` improvised into a shell pipeline.

## The privilege

`dwed` is started by peinit as an ordinary service with `Identity = SYSTEM`. It constructs no tokens of its own; the privilege arrives entirely from that one line in its service definition. Asked on a running machine, its token reports:

```text
user          Local System (S-1-5-18)
type          Primary
logon_type    5 (Service)
privileges    36, all enabled — including SeCreateToken, SeTcb,
              SeAssignPrimaryToken, SeDebug, SeBackup, SeRestore,
              SeImpersonate, SeLoadDriver, SeSecurity, SeAudit
```

This is the same privilege peinit itself holds, deliberately. A full `SYSTEM` account is not reachable from a console at all, and reaching one is the single capability that makes `dwed` worth having over a serial login.

It is also why the rest of this page is about containment.

## The security posture

`dwed` does not authenticate its peer, and **cannot**.

The transport is [vsock](/peios/developing-for-peios/dwe/protocol.md), which crosses a hypervisor boundary between two separate kernels. A guest kernel can be told a peer's context id, but it cannot *attest* anything about who is behind it — those are claims, not attestation, and no amount of work inside the guest changes that. Peios' identity model rules `AF_VSOCK` out as a carrier of process identity for exactly this reason.

So authentication is the job of whatever surrounds the machine — the host it runs on, the network it sits behind — and never of `dwed`.

Three things follow, and all three are load-bearing:

**It is never published as a package.** The `peios-dwe` package does not exist in the public repository and never will. It reaches a machine only inside a dedicated `peios-dwe` ISO, so it cannot arrive anywhere by way of an ordinary install.

**Distribution is the only real control.** The usual advice — "do not install it in production" — does not apply cleanly, because the machines DWE is wanted on are production in every sense except intent. There is no honest way to enforce the distinction from inside the software. Keeping it out of the repository is what stops it turning up somewhere by accident.

**Installing it is not enough to start it.** A package may ship a service definition but may not start it: the definition sits inert in the vendor seed library until an image names it in `[registry] autoapply`. For `dwed`, that opt-in is the moment a machine becomes remotely ownable — so it is a decision the image makes explicitly, not a consequence of a package being present.

## What DWE is not

**It is not a test harness.** Provium covers deterministic, repeatable testing of a whole system, from initramfs through to network interaction, and does it far better than anything built on `dwed` could. Tests belong there.

DWE is for the case Provium cannot serve: a machine that is *already* running, already misbehaving, and needs to be asked questions nobody thought to write down in advance. When a Provium test fails for a reason that is not obvious, DWE is how you go and look.

**It is not a general remote-administration tool.** There is no session model, no pty, no terminal multiplexing, and no plan for any of them until something concrete needs one. `dwed` is a way to ask a running machine questions, and the machine — not the connection — is the thing that persists.

> [!NOTE]
> `dwed` is a phase-2 service, so a boot that breaks before phase 2 has no DWE at all. It also goes down with a machine that wedges completely. Both are accepted limits: below that line, a serial console is still the tool.

## Next

- [Driving a machine](/peios/developing-for-peios/dwe/driving-a-machine.md) — booting with a vsock device, and the `dwe` command.
- [The DWE protocol](/peios/developing-for-peios/dwe/protocol.md) — the wire format, for building against it directly.

---

# Driving a machine

_Peios / Developing for Peios / DWE_

> Booting a Peios machine with a vsock device, pointing the dwe client at it, and running commands, moving files and detaching long work that outlives the connection.

This page assumes an image built with the `peios-dwe` package and its service seed applied. If you are not sure, [What DWE is](/peios/developing-for-peios/dwe/what-dwe-is.md) explains why both are needed and why neither is the default.

## Give the machine a transport

`dwed` binds a vsock listener at boot, but a guest cannot conjure the transport itself — the hypervisor has to give it a vsock device. Under QEMU that is one flag:

```sh
-device vhost-vsock-pci,guest-cid=3
```

The context id is how the host addresses this guest. Any value of 3 or above works; QEMU refuses to start if another running guest already holds the one you picked, so concurrent machines need distinct ids.

In the Peios tree, `make boot-dwe` is `make boot` with that device attached:

```sh
cd dist/prod
make boot-dwe              # or: make boot-dwe DWE_CID=4
```

Leave it running. Unlike a scripted boot, the point is that the machine stays up.

> [!NOTE]
> `/dev/vhost-vsock` is owned by `root:kvm`, so membership of the `kvm` group is enough — this does not need root.
>
> On a guest booted *without* a vsock device, `dwed` reports that it has no transport and exits cleanly. That is not an error and does not restart-loop.

## Point the client at it

The `dwe` client takes its target from `--target` or from `DWE_TARGET`:

```sh
export DWE_TARGET=vsock:3:4820        # a VM by context id
export DWE_TARGET=tcp:10.0.0.5:4820   # or over the network
```

The port defaults to 4820 and can be left off. Confirm you have the machine you think you have:

```console
$ dwe info
dwed            0.1.0
protocol        1
boot id         79880a4b-e3d0-4992-bea2-eb56f3839711
uptime          193s
```

The **boot id** is worth reading. It changes on every boot, so it is how you tell "the machine rebooted under me" from "my connection dropped" — two situations that otherwise look identical and mean very different things.

## Run something

```console
$ dwe exec -- ls -l /system
$ dwe exec --cwd /tmp -- ./probe
```

Everything after `--` is the argument vector, executed **directly**. There is no shell, so nothing re-interprets your quoting, globs your arguments or splits them on spaces. When you want a shell, ask for one:

```sh
dwe exec -- sh -c 'for p in /proc/[0-9]*; do cat $p/comm; done'
```

Two behaviours matter more than they look:

**The exit status is yours.** `dwe exec` exits with the guest command's status, so ordinary shell chaining works:

```sh
dwe exec -- test -f /etc/hostname && echo "it is there"
```

A command killed by a signal exits `128+N`, matching shell convention, so it is distinguishable from one that merely failed.

**The streams stay apart.** The guest's `stdout` and `stderr` are written to *your* `stdout` and `stderr`, still separated, as raw bytes:

```console
$ dwe exec -- ls /nonexistent 2>errors.txt
$ cat errors.txt
ls: cannot access '/nonexistent': No such file or directory
```

## Move files

```sh
dwe pull /var/log/peinit.log ./peinit.log   # out of the guest
dwe pull /etc/hostname                      # or straight to stdout
dwe push ./probe.sh /tmp/probe.sh           # into the guest
echo "hello" | dwe push - /tmp/greeting     # from stdin
```

Transfers are binary-safe and whole-file. Encoding is handled inside the protocol, so you never have to improvise `base64` through a shell pipeline to get a binary out intact.

## Work that outlives the connection

This is the part that makes a long investigation possible. `--detach` returns a job handle immediately, and the job keeps running when the connection closes:

```console
$ dwe exec --detach -- make -C /src world
1
```

Come back whenever — a minute later, an hour later, over as many separate connections as you like:

```console
$ dwe jobs
1      running      make -C /src world

$ dwe output 1
[... everything so far ...]

$ dwe output 1 --follow          # or watch it live
$ dwe signal 1 15                # or stop it
```

### Reading only what is new

Polling a job repeatedly with plain `dwe output` re-reads everything from the start. To pick up where you left off, ask for the offsets and pass them back:

```console
$ dwe output 1 --offsets
[... output ...]
--since-stdout 4096 --since-stderr 128

$ dwe output 1 --since-stdout 4096 --since-stderr 128
[... only what arrived since ...]
```

The cursor is yours to keep rather than something `dwed` tracks. It has no sessions and cannot tell two callers apart, so a server-side cursor would have two people polling the same job eating each other's output.

> [!IMPORTANT]
> Job state lives in memory. A restart of `dwed` restores the listener but loses every job and everything it had captured. Output is capped at 16 MiB per stream, after which the tail is dropped and the reply says so — pipe a genuinely large job to a file in the guest and `pull` it instead.

## When it does not answer

**`cannot reach vsock:3:4820`** — the machine is not running, has no vsock device, or is using a different context id. Check the QEMU command line for `vhost-vsock-pci`.

**The connection opens but nothing answers** — `dwed` is not running in the guest. Its service definition was shipped but never applied: an image has to name `dwed-service.reg` in `[registry] autoapply` for anything to start. On the console, look for `peinit: service dwed started`.

**`protocol mismatch`** — `dwe` and `dwed` are from different builds. The wire version is checked rather than guessed at, so this is reported instead of being allowed to misparse. Rebuild both.

**Nothing at all, and the machine is wedged** — `dwed` goes down with the machine it is debugging. Below that line the serial console is still the tool.

---

# The DWE protocol

_Peios / Developing for Peios / DWE_

> The dwed wire protocol — newline-delimited JSON over vsock or TCP, the request and reply shapes for every operation, error codes, and the transport and versioning rules.

`dwed` speaks **newline-delimited JSON**: one JSON object per line in each direction, with each response carrying the `id` of the request it answers.

JSON rather than a compact binary encoding is a deliberate trade. When `dwed` itself is the thing misbehaving, the protocol has to stay drivable by hand — and being able to type at it and read what comes back is worth more than the bytes a binary framing would save:

```console
$ nc 10.0.0.5 4820
{"id":1,"op":"info"}
{"id":1,"ok":{"reply":"info","protocol_version":1,"dwed_version":"0.1.0",...}}
```

## Framing

A request is an object with an `id` and an `op`, plus that op's arguments inline:

```json
{"id": 7, "op": "exec", "argv": ["ls", "-l", "/system"]}
```

`id` is chosen by the client and echoed back untouched. Requests on one connection are answered in order; concurrency comes from opening more connections, or from detaching work.

A response carries the same `id` and exactly one of `ok` or `error`:

```json
{"id": 7, "ok": {"reply": "exec", "exit": 0, "stdout": "…", "stderr": "", "truncated": false}}
{"id": 7, "error": {"code": "no_such_job", "message": "job 9 is unknown"}}
```

The `reply` field names the shape of the payload. It is there because several replies carry the same fields — `exec` and `job.output` both have `stdout`, `stderr` and an optional `exit` — and a reader should never have to guess which one it is holding from shape alone.

A request that does not parse is still answered, with `id: 0` and a `bad_request` error. Silence would leave a client waiting on an `id` that is never coming, which presents as a hang — the one symptom hardest to tell apart from the bug being investigated.

### Bytes

Every field carrying payload bytes is **base64**: file contents, and captured `stdout`/`stderr` alike.

Output is base64 rather than a JSON string because a guest command's output is not guaranteed to be valid UTF-8, and a tool that mangles a binary is worse than one that refuses it. Clients decode and write raw bytes back out, so the encoding never reaches whoever is driving the tool.

## Operations

### `exec`

Run a command. Synchronous unless `detach` is set.

| Field | | |
|---|---|---|
| `argv` | required | Program and arguments. Executed directly — **not** through a shell. |
| `cwd` | optional | Working directory. |
| `env` | optional | Extra environment, as `[name, value]` pairs, on top of the service's own. |
| `stdin` | optional | Bytes written to the child's stdin, which is then closed. |
| `detach` | optional | Return a job handle immediately instead of waiting. |

Replies `exec` with `exit` (null if signalled), `signal`, `stdout`, `stderr` and `truncated`; or `job` with a `job` handle when `detach` was set.

Both output streams are captured concurrently. A child that fills one pipe while the other goes undrained would otherwise deadlock, and "the command hung" is the least useful thing a debugging tool can report.

### `job.list`

No arguments. Replies `job_list` with a `jobs` array of `{job, argv, running, exit, signal, started}`.

Jobs outlive the connection that created them. They do not outlive `dwed` itself.

### `job.output`

| Field | | |
|---|---|---|
| `job` | required | The handle. |
| `since_stdout` | optional | Resume stdout from this byte offset. |
| `since_stderr` | optional | Resume stderr from this byte offset. |

Replies `job_output` with `stdout`, `stderr`, `stdout_next`, `stderr_next`, `running`, `exit`, `signal` and `truncated`.

The `*_next` values are what to pass as `since_*` on the following call. Cursors are the caller's to keep: `dwed` has no sessions and cannot distinguish two callers, so a server-side cursor would have concurrent readers consuming each other's output. An offset past the end is clamped rather than rejected.

### `job.signal`

`{job, signal}` — deliver a signal to a running job. Replies `done`. A job that has already exited gives `not_running`.

### `file.read`

`{path}` — replies `file_read` with `bytes` and `mode`.

### `file.write`

`{path, bytes, mode?}` — replies `done`. `mode` is applied after writing.

### `info`

No arguments. Replies `info`:

| Field | |
|---|---|
| `protocol_version` | The version `dwed` speaks. |
| `dwed_version` | Its own release. |
| `boot_id` | Distinguishes one boot from the next. |
| `uptime` | Seconds since boot. |

`boot_id` is the field worth using. It lets a client tell "the machine rebooted under me" from "my connection dropped" — two situations that look identical from the socket and mean entirely different things.

## Errors

| `code` | |
|---|---|
| `bad_request` | Malformed JSON, an unknown op, or empty `argv`. |
| `io` | An underlying system call failed. Carries `errno`. |
| `no_such_job` | No job with that handle. |
| `not_running` | The job has already exited. |

`errno` is carried separately from the message so a client can match on the cause rather than parse English.

## Transports

**vsock** is the default, on port 4820. It needs no networking in the guest — which matters, because a machine whose networking is part of what broke is squarely one of the cases DWE exists for. `dwed` binds `VMADDR_CID_ANY`: a guest does not reliably know its own context id, and does not need to.

**TCP** is available on the same port but is **opt-in** (`dwed --tcp 0.0.0.0:4820`). Listening by default would hand the machine to anyone who can route to it, which is more than starting a service should quietly do given there is nothing behind it.

The protocol is identical over either.

> [!CAUTION]
> Neither transport authenticates anything. Whatever reaches the socket owns the machine as `SYSTEM`. See [the security posture](/peios/developing-for-peios/dwe/what-dwe-is.md#the-security-posture).

## Versioning

`PROTOCOL_VERSION` is bumped whenever any wire type changes shape. A client compares it against `info` and reports a mismatch rather than misparsing a response it half understands.

There is no negotiation and no compatibility window. Both halves ship from one repository and are built together; a version check is there to give a clear error, not to bridge a gap.
