# Peios Events Index

> Every event Peios emits, in one place — the KMES envelope, the shared subject and process records, and a field-by-field schema for each event from the kernel, the filesystem, the registry, peinit, peipkg and eventd.

---

# 1.1 What an Event Is

_Peios / Using Peios / Events / Introduction_

> What counts as an event on a Peios system, the two transports that carry them, and what this book deliberately leaves out.

An **event** is a record that something happened: an access was checked,
a service started, a package was installed, a descriptor was found
corrupt. Events are produced by the component that observed the thing,
and consumed by whatever is watching — usually `eventd`, sometimes a
tool reading the stream directly.

This book enumerates every event Peios emits, with the fields each one
carries. It is a lookup, not an explanation. Where an event's *meaning*
needs the mechanism behind it, the chapter links to the manual that
describes that mechanism.

## 1.1.1 Two transports, not one

Most events travel through **KMES**, the kernel message event stream: a
per-CPU ring buffer that the kernel writes into and userspace reads
from. Every KMES event is a binary header followed by a msgpack
payload. Chapters 3 to 7 document KMES events.

Two things in this book are not KMES events, and are included because an
operator looking for "what does Peios tell me" would otherwise miss
them:

- **eventd's synthetic events** (§8) are written straight into a shard
  database and never touch KMES. They carry no header stamps.
- **LCS watch records** (§5.4) are binary records read from a key file
  descriptor. They are a notification mechanism, not an audit trail, and
  their format has nothing in common with a KMES event.

## 1.1.2 What is not here

This book does not cover **logs** or **metrics**. Both reach eventd by a
different path, are stored in different tables, and are not events. The
eventd manual covers them.

Nor does it cover the query language for reading events back. That is
PSPU §3, with the operator-facing view in the eventd manual.

---

# 1.2 The Envelope

_Peios / Using Peios / Events / Introduction_

> The packed binary header in front of every KMES payload — the fields the kernel stamps, the layout, and what ordering you get.

Every KMES event is a packed binary header followed immediately by its
msgpack payload, delivered as one contiguous byte sequence with no
padding anywhere.

The emitter supplies only two things: the **event type string** and the
**payload**. Everything else in the header is stamped by KMES itself,
which is what makes the identity fields trustworthy — an emitter cannot
forge them.

## 1.2.1 Fields KMES stamps

| Field | Meaning |
|---|---|
| `timestamp` | Wall clock at the moment KMES accepted the event, nanoseconds since the Unix epoch. |
| `sequence` | The emitting CPU's per-boot counter. The first event on each CPU gets 1. |
| `cpu_id` | The CPU whose ring buffer holds the event. |
| `origin_class` | 0 for syscall emission, unconditionally. For kernel emission, the value the calling subsystem passed. |
| identity GUIDs | The effective, true and process token GUIDs of the task that caused the event. |

The identity stamps matter for reading this book: **several events carry
no caller in their payload at all**, because the envelope already
names one. StrataFS copy-up records are the clearest case — nothing in
the payload names a token, and the caller is recovered from the header.

`event_size`, `header_size` and `type_len` are structural, computed by
KMES during construction.

## 1.2.2 Layout

All fields before the event type string sit at fixed offsets. The type
string begins at offset 77, with its `u16` length at offset 75, so the
header is exactly `77 + type_len` bytes. The payload runs from
`header_size` to `event_size`, and the next event begins at
`event_size` from the start of the current one.

All multi-byte header integers are little-endian. The identity GUIDs are
opaque 16-byte values.

The full field-by-field layout is normative in PSPK §2, the KMES event
stream specification. This summary is enough to walk a stream; it is not
enough to implement one.

## 1.2.3 Ordering

`timestamp` is captured before `sequence` is assigned, so two events
with the same timestamp on the same CPU are ordered by sequence.

Across CPUs there is no global order. Two events on different CPUs with
close timestamps may have been observed in either order.

---

# 1.3 Encoding Conventions

_Peios / Using Peios / Events / Introduction_

> Every payload is a msgpack map with string keys — how the recurring value types are represented, and the three event-type naming styles.

Every KMES payload in this book is a **msgpack map with UTF-8 string
keys**. The key set is stable per event type.

## 1.3.1 Value representations

The same conceptual types appear across many events and are always
encoded the same way.

| Conceptual type | msgpack representation |
|---|---|
| SID | **bin** holding the binary SID, 8–68 bytes. |
| GUID | **bin**, exactly 16 bytes. |
| ACE | **bin** holding the binary ACE, copied from the descriptor. |
| Access mask | **uint**, 32-bit. |
| Boolean | **bool**. |
| Privilege name | **string**, UTF-8, e.g. `SeBackupPrivilege`. |
| Process ID | **uint**. |
| Path or name | **string**, UTF-8. |
| Timestamp | **uint**, unless an event's schema says otherwise. |
| Object context | **bin** or **nil**. An opaque caller-supplied blob; its contents are service-specific. |

Binary SIDs, GUIDs and ACEs are carried as bytes rather than as text
because they are compared as bytes. A textual SID would have to be
parsed back before it could be matched.

## 1.3.2 Event type strings use three different styles

There is no single convention. What an event type looks like depends on
which component emits it:

| Style | Emitters | Examples |
|---|---|---|
| `kebab-case` | KACS | `access-audit`, `logon-session-destroyed` |
| `dotted.snake` | peinit, peipkg, eventd | `job.created`, `peipkg.repo-add`, `synthetic.config_change` |
| `SCREAMING_SNAKE` | StrataFS, LCS | `STRATAFS_COPY_UP`, `LCS_BACKUP_START` |

The dotted family is not internally consistent either: `peipkg.repo-add`
is dotted-then-kebab while `synthetic.config_change` is
dotted-then-snake.

This is recorded because a consumer matching event types has to know it,
not because it is defended. Match the exact strings in this book rather
than deriving one from a pattern.

---

# 1.4 Reading an Event

_Peios / Using Peios / Events / Introduction_

> The four rules every consumer of this stream lives by — ignore unknown keys, do not read meaning into absence, expect loss, and do not trust the contents.

Four rules govern every consumer of this stream.

## 1.4.1 Ignore unknown keys

Future versions may add fields to an event without changing the existing
ones. A consumer that processes the keys it knows and ignores the rest
keeps working across upgrades. A consumer that rejects unrecognised keys
breaks on the first addition.

## 1.4.2 Do not rely on a key being absent

A field that is optional today may become always-present later. Absence
is not a signal.

## 1.4.3 Delivery is best-effort

KMES is a ring buffer. The kernel writes; keeping up is the subscriber's
problem.

- A subscriber that falls behind **loses events**. eventd notices and
  records a `synthetic.gap` (§8.1), which is how a gap becomes visible
  rather than silent.
- **There is no replay.** An event missed is gone. Nothing can ask for
  it back.
- **Buffers are per-subscriber.** One slow reader does not affect
  another.
- **Order is per-subscriber**, not global.

For durable audit, read events from eventd's stores rather than from
KMES directly. eventd drains its subscription continuously and persists
what it reads; from that point the store is the record, not the ring.

## 1.4.4 Events are not authenticated

Events are trusted because they came from the kernel through KMES, not
because they are signed. Nothing in an event carries a signature.

Cryptographic non-repudiation is a userspace concern applied after
events leave the kernel. If a deployment needs it, it is added on the
far side of eventd, not here.

## 1.4.5 Versioning

Event types are not versioned by a field. The schemas in this book are
stable: fields may be added, but an existing field will not be renamed,
retyped or removed under the same type string.

A change that would break compatibility changes the **type string**
instead — `access-audit` would become `access-audit-v2` — so an existing
consumer keeps receiving the shape it understands and simply never sees
the new one. No type in this book has been versioned that way.

---

# 2.1 The Subject Record

_Peios / Using Peios / Events / Common Records_

> The subject map identifying the effective token behind an operation — its fields, the two parallel arrays, and what it deliberately omits.

The `subject` map identifies the **effective token** under which an
operation ran. It appears in every KACS event except
`logon-session-destroyed`.

For an event fired from an impersonating thread, the subject is the
impersonation token, not the primary. For a non-impersonating thread the
primary token *is* the effective one.

For `continuous-audit` (§3.2) the subject is the effective token **at
the moment of the operation**, not at the moment the handle was opened.
A process whose token changed since the open gets the current subject on
each subsequent operation.

## 2.1.1 Fields

| Key | Type | Meaning |
|---|---|---|
| `user_sid` | bin | The token's user SID. |
| `group_sids` | array of bin | The token's group SIDs. |
| `group_attributes` | array of uint | Per-group attribute bitmasks, parallel to `group_sids`. |
| `integrity_level` | uint | The token's integrity RID — 0, 4096, 8192, 12288 or 16384. |
| `pip_type` | uint | The calling process's PIP type. 0 None, 512 Protected, 1024 Isolated. |
| `pip_trust` | uint | The calling process's PIP trust level. |
| `auth_id` | uint | The LUID of the logon session the token belongs to. |
| `token_id` | uint | The token's own LUID. |
| `impersonation_level` | uint | 0–3. A primary token reports 0. |
| `projected_uid` | uint | The Linux UID projection, for correlating with Linux-side audit data. |

Every field is always present.

`auth_id` is the join key to `logon-session-destroyed` (§3.5) and to
`/sys/kernel/security/kacs/sessions`. `token_id` correlates events from
one specific token.

## 2.1.2 The two parallel arrays

`group_sids[i]` and `group_attributes[i]` describe the same group entry,
and the arrays are always the same length.

| Flag | Value | Meaning |
|---|---|---|
| `SE_GROUP_MANDATORY` | 0x01 | Cannot be disabled. |
| `SE_GROUP_ENABLED_BY_DEFAULT` | 0x02 | Enabled at creation. |
| `SE_GROUP_ENABLED` | 0x04 | Currently enabled. |
| `SE_GROUP_OWNER` | 0x08 | May act as owner for new objects. |
| `SE_GROUP_USE_FOR_DENY_ONLY` | 0x10 | Matches deny ACEs only. |
| `SE_GROUP_INTEGRITY` | 0x20 | Identifies an integrity SID. Present for ABI parity; MIC reads the token's `integrity_level` field, not this flag. |
| `SE_GROUP_INTEGRITY_ENABLED` | 0x40 | Used with `SE_GROUP_INTEGRITY`. |
| `SE_GROUP_RESOURCE` | 0x20000000 | A domain-local group from a resource domain. Metadata only. |
| `SE_GROUP_LOGON_ID` | 0xC0000000 | The logon SID. Cannot be disabled. |

These are MS-DTYP's names, which PCDS uses. The headers declare the same
flags as `KACS_SID_GROUP_*`; the Peios Kernel TRM §3.A maps the two.

Reconstructing group membership from an event means applying the same
rule the access check applies: `SE_GROUP_ENABLED` set and
`SE_GROUP_USE_FOR_DENY_ONLY` clear, for allow-side matching.

## 2.1.3 What the subject deliberately omits

Privileges, claims, the restricted-SID list, confinement state, and the
default DACL are all absent. Each is unbounded, and an event that
embedded them could grow without limit.

Code needing full token state queries the token directly with
`KACS_IOC_QUERY`, while it still exists. The subject record is for
correlation, not for reconstruction.

---

# 2.2 The Process Record

_Peios / Using Peios / Events / Common Records_

> The process map identifying where an event came from, what to correlate on, and why there is no thread ID.

The `process` map identifies the process the event came from. It appears
in every KACS event except `logon-session-destroyed`, which has no
causing process.

| Key | Type | Meaning |
|---|---|---|
| `pid` | uint | The process ID. |
| `name` | string | The kernel's name for the process, typically the executable's basename. |
| `executable_path` | string | The path resolved at exec, with symlinks already followed. |

Every field is always present. For `continuous-audit` this is the
operation-time process, not the one that opened the handle.

## 2.2.1 Correlating on it

`pid` is reliable only in the short term. Process IDs are reused, so a
`pid` in a week-old record may name something unrelated. For durable
records, correlate on `name` and `executable_path`.

`name` is the kernel's internal name. It is not `argv[0]`, and a process
that rewrote its argv is unaffected here.

## 2.2.2 No thread ID

The record identifies a process, not a thread. Events that fire on one
specific thread still report only the process. Nothing in the current
event set carries a `tid`.

---

# 2.3 The Caller Summary

_Peios / Using Peios / Events / Common Records_

> The registry's own identity submap, why LCS carries it instead of the subject record, and how the two compare field by field.

LCS uses its own identity submap, `caller`, rather than the subject
record. Six of its seven audit events carry it.

It exists separately because LCS's events are emitted from a different
subsystem with a different bound on what it will serialise. The two
records overlap but are not interchangeable, and a consumer handling
both needs to read each on its own terms.

| Key | Meaning |
|---|---|
| `effective_token_guid` | The token the operation ran under. |
| `true_token_guid` | The underlying token, where impersonation is in play. |
| `process_guid` | The calling process. |
| `user_sid` | The effective token's user SID. |
| `authentication_id` | The logon session LUID. |
| `token_id` | The token's own LUID. |
| `token_type` | Primary or impersonation. |
| `impersonation_level` | 0 for a primary token. |
| `integrity_level` | The token's integrity RID. |

Nine fields, and no more. Group lists, privilege arrays, claims and
default DACLs are unbounded and are never included — the same reasoning
that shapes the subject record (§2.1), applied independently.

## 2.3.1 Against the subject record

| | `subject` | `caller` |
|---|---|---|
| Emitted by | KACS | LCS |
| Identity by | SIDs | GUIDs, plus `user_sid` |
| Groups | `group_sids` with attributes | absent |
| PIP state | `pip_type`, `pip_trust` | absent |
| Linux projection | `projected_uid` | absent |
| Session join key | `auth_id` | `authentication_id` |

Note the session join key is spelled differently in each. Correlating a
KACS event with an LCS event on the same logon session means matching
`subject.auth_id` against `caller.authentication_id`.

---

# 3.1 access-audit

_Peios / Using Peios / Events / Kernel Access Events_

> The most common event in the system — what triggers it, why one access can produce several, and a worked example.

The most common event in the system. Fires at AccessCheck completion,
from the SACL audit walk, and from a token's `audit_policy` forcing an
audit that no ACE asked for.

Event type string: `access-audit`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1). |
| `object_context` | bin or nil | Caller-supplied opaque identifier for the object. `nil` if AccessCheck was not given one. |
| `requested_access` | uint | The mask the caller requested, after generic mapping. |
| `granted_access` | uint | The mask actually granted. |
| `success` | bool | True when every requested bit is in `granted_access`. |
| `trigger` | map | Why this event fired. Below. |
| `process` | map | Process record (§2.2). |

Every field is always present.

## 3.1.1 The trigger record

| Key | Type | Meaning |
|---|---|---|
| `kind` | string | `sacl` or `policy`. |
| `ace` | bin or nil | For `kind = sacl`, the matched ACE's bytes. For `kind = policy`, `nil`. |

`kind = sacl` means an audit ACE in the object's SACL — or in a central
access policy's SACL — matched this access, and `ace` carries the exact
ACE so a consumer can identify which rule fired.

`kind = policy` means nothing in any SACL asked for this. The audit
fired because the calling token's `audit_policy` carries
`OBJECT_ACCESS_SUCCESS` or `OBJECT_ACCESS_FAILURE`, which forces an
audit on every access that token makes.

The distinction matters when reading volume. A flood of `kind = policy`
events is a property of the token, and is fixed by changing the token's
policy. A flood of `kind = sacl` events is a property of the object, and
is fixed by changing its SACL.

## 3.1.2 One access can produce several events

The event is per matching audit ACE, not per access. An access that
matches three audit ACEs produces three events, each with a different
`ace` in its trigger and otherwise identical.

## 3.1.3 Example

A successful read where a SACL audit ACE matched:

```
{
  "event_type": "access-audit",
  "event_time": <timestamp>,
  "subject": { ... },
  "object_context": <bin>,
  "requested_access": 0x00120089,
  "granted_access":   0x00120089,
  "success": true,
  "trigger": { "kind": "sacl", "ace": <bin> },
  "process": { ... }
}
```

`0x00120089` is `GENERIC_READ` after mapping to file-specific bits.
Generic bits never survive into an event; the mask is always specific.

---

# 3.2 continuous-audit

_Peios / Using Peios / Events / Kernel Access Events_

> Per-operation auditing on an already-open handle — the three masks involved, and why the subject is re-read each time.

Fires per operation on an already-open handle, when the operation's
required access overlaps a continuous audit mask cached on that handle.

Where `access-audit` records the decision to open something,
`continuous-audit` records what was then done with it. The mask is
configured by `SYSTEM_ALARM*` ACEs at the access check that opened the
handle, and the event is fired afterwards by whatever kernel subsystem
enforces the operation — FACS for file handles.

Event type string: `continuous-audit`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1), reflecting the **operation-time** effective token. |
| `object_context` | bin or nil | Object identifier. May differ from the open-time context if the enforcement point keeps its own. |
| `operation` | string | The operation name. FACS uses a `file.` prefix — `file.read`, `file.write`, `file.fallocate`. Other enforcement points use their own. |
| `requested_access` | uint | The mask this specific operation needs. |
| `matched_access` | uint | The subset of `requested_access` that overlapped the handle's continuous audit mask. |
| `granted_access` | uint | The mask cached on the handle at open time. |
| `success` | bool | Whether the operation itself succeeded. |
| `process` | map | Process record (§2.2), operation-time. |

Every field is always present.

## 3.2.1 The three masks

They are easy to confuse, and reading them together is the point of the
event.

- **`requested_access`** is what the *operation* needs. A read needs
  `FILE_READ_DATA`; a write needs `FILE_WRITE_DATA`; an `mmap` with
  `PROT_EXEC` needs `FILE_EXECUTE`.
- **`matched_access`** is `requested_access` intersected with the
  handle's continuous audit mask. This is why the event exists — the
  bits that triggered it.
- **`granted_access`** is what the handle was opened with. An operation
  can only succeed if `requested_access` is a subset of it.

Read as a sentence: *the operation needed these bits, of which these
triggered audit, against a handle opened with these, and it
succeeded or did not.*

## 3.2.2 Why the subject is re-read

A handle outlives the token that opened it. A process that changes its
effective token — starting or stopping impersonation — keeps its
handles, and every subsequent operation is audited under the token
current at that moment.

An investigation that assumes the open-time identity for later
operations will attribute them to the wrong principal.

## 3.2.3 Example

A read on a file carrying an alarm ACE on `FILE_READ_DATA`:

```
{
  "event_type": "continuous-audit",
  "event_time": <timestamp>,
  "subject": { ... },
  "object_context": <bin>,
  "operation": "file.read",
  "requested_access": 0x00000001,
  "matched_access":   0x00000001,
  "granted_access":   0x00120089,
  "success": true,
  "process": { ... }
}
```

---

# 3.3 privilege-use

_Peios / Using Peios / Events / Kernel Access Events_

> Recording a privilege that contributed bits to the granted mask — the five that can appear, and what success actually means here.

Fires at AccessCheck for a privilege that contributed bits to the
granted mask, when the token's `audit_policy` asks for it.

Event type string: `privilege-use`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1). |
| `object_context` | bin or nil | Object identifier from the access check. |
| `privilege` | string | The canonical privilege name. |
| `requested_access` | uint | The bits the caller requested that this privilege might address. |
| `granted_access` | uint | The bits the privilege contributed, before later narrowing. |
| `surviving_access` | uint | The subset of `granted_access` that reached the final granted mask. |
| `success` | bool | True when `surviving_access` is non-empty. |
| `process` | map | Process record (§2.2). |

Every field is always present.

## 3.3.1 Only five privileges can appear

The `privilege` field carries a canonical name, and **only five are
representable**:

- `SeSecurityPrivilege`
- `SeTakeOwnershipPrivilege`
- `SeBackupPrivilege`
- `SeRestorePrivilege`
- `SeRelabelPrivilege`

Any other bit fails the encoder closed rather than emitting an unnamed
privilege. This is consistent with those being the only five that can
influence an access check at all, and therefore the only five that can
produce this event.

A consumer will never see a sixth name here. A privilege used for
something other than an access decision produces no `privilege-use`
event.

## 3.3.2 Success means it worked, not that it fired

This is the field most often misread.

`success = true` — the privilege contributed bits and they survived to
the final grant. The privilege did useful work. Governed by the
`PRIVILEGE_USE_SUCCESS` audit policy.

`success = false` — the privilege contributed bits and a later layer
stripped them. The caller was in a confinement that does not permit it,
or a CAAP rule narrowed it out, or the caller was non-dominant under
PIP. Governed by `PRIVILEGE_USE_FAILURE`.

A `success = false` event is not a failed attempt to *use* a privilege.
It is a privilege that fired and was then overridden — which is usually
the more interesting record of the two, because it shows a boundary
doing its job.

A token can carry either policy bit, both, or neither, and each controls
its own flavour independently.

## 3.3.3 Example

A backup tool whose `SeBackupPrivilege` was stripped by confinement:

```
{
  "event_type": "privilege-use",
  "event_time": <timestamp>,
  "subject": { ... },
  "object_context": <bin>,
  "privilege": "SeBackupPrivilege",
  "requested_access": 0x00000001,
  "granted_access":   0x00000001,
  "surviving_access": 0x00000000,
  "success": false,
  "process": { ... }
}
```

---

# 3.4 caap-policy-diagnostic

_Peios / Using Peios / Events / Kernel Access Events_

> Two unrelated central-access-policy conditions sharing one event type, told apart by the kind field — and the limits of the mismatch report.

Fires during the central access policy step of AccessCheck, for two
unrelated conditions distinguished by the `kind` field: a CAAP SACL that
failed to evaluate, and a staged policy that would have decided
differently from the effective one.

Event type string: `caap-policy-diagnostic`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1). |
| `object_context` | bin or nil | Caller-supplied object identifier. |
| `kind` | string | `sacl-error` or `staging-mismatch`. |
| `phase` | string | Which phase of CAAP evaluation produced the diagnostic. |
| `policy_sid` | bin or nil | The policy involved. `nil` for `staging-mismatch`. |
| `rule_index` | uint or nil | The rule involved. `nil` for `staging-mismatch`. |
| `reason` | string or nil | Diagnostic text, for `sacl-error`. |
| `requested_access` | uint | The mask the caller requested. |
| `effective_granted_access` | uint | Total granted under the effective policy. |
| `staged_granted_access` | uint | Total the staged policy would have granted. |
| `object_results_differ` | bool | Whether staged and effective differed for this object. |
| `process` | map | Process record (§2.2). |

Every key is always present; several are `nil` depending on `kind`.

## 3.4.1 kind = sacl-error

A central access rule's SACL could not be evaluated. `policy_sid`,
`rule_index` and `reason` identify what failed and where.

This is a defect in the policy, not in the access. The access is still
decided; the event says a rule that should have participated could not.

## 3.4.2 kind = staging-mismatch

A staged policy — one being trialled before it takes effect — would have
produced a different result from the policy actually in force. This is
the mechanism's whole purpose: run the new policy in parallel and report
where it would have changed something, before it can break anything.

`policy_sid` and `rule_index` are `nil` here. The mismatch is a property
of the whole evaluation, not attributable to one rule.

## 3.4.3 The limits of the mismatch report

Worth knowing before building anything on it.

It carries the two total granted masks and a boolean. It does **not**
identify which rule differed, and it does not report which audit events
would have changed.

For a mismatch that is purely in SACL behaviour, the two masks can be
**equal** while `object_results_differ` is true. A consumer comparing
only the masks will conclude nothing changed.

---

# 3.5 logon-session-destroyed

_Peios / Using Peios / Events / Kernel Access Events_

> The event fired when a logon session loses its last token reference — why it has no subject or process, and why nothing matches it at creation.

Fires when a logon session loses its last token reference and the kernel
destroys it.

Event type string: `logon-session-destroyed`.

| Key | Type | Meaning |
|---|---|---|
| `session_id` | uint | The destroyed session's LUID. |
| `user_sid` | bin | The session's user SID. |
| `logon_type` | uint | Interactive, Network, and so on. |
| `auth_package` | string | The authenticating package — `Kerberos`, `NTLM`, `local`. |
| `created_at` | uint | When the session was created. |

Every field is always present.

## 3.5.1 No subject, no process

This is the only KACS event with neither. The session *was* the subject,
and it has just ended. No process caused it — the last reference simply
went away, which may have been any process exiting, or none in
particular.

## 3.5.2 Correlating

`session_id` is the same LUID that every token in that session reported
as `subject.auth_id` (§2.1), and that LCS events report as
`caller.authentication_id` (§2.3). It is the join key for everything
that session ever did.

With `created_at`, the event bounds a session's whole lifetime, which is
what makes it useful for reconstructing a login after the fact.

The event fires **exactly once** per session. Consumers — authd
especially — use it to release session-scoped state: Kerberos tickets,
cached directory data, per-session credentials.

## 3.5.3 There is no matching creation event

Nothing is emitted when a session is created, so the pair is asymmetric.
See §3.6 for what else is absent and why.

---

# 3.6 corrupt-sd

_Peios / Using Peios / Events / Kernel Access Events_

> What FACS emits on finding a structurally invalid descriptor on a file, why it is rate-limited, and the related events that do not exist.

Fires when FACS encounters a structurally invalid security descriptor on
a file.

Event type string: `corrupt-sd`.

| Key | Type | Meaning |
|---|---|---|
| `subject` | map | Subject record (§2.1) for the access that triggered detection. |
| `object_context` | bin or nil | Identifier of the object whose descriptor was corrupt. |
| `reason` | string | What was wrong — `sd_too_large`, `acl_malformed`, `sid_invalid`. |
| `process` | map | Process record (§2.2). |

Every field is always present.

The subject is whoever happened to touch the file, not whoever caused
the corruption. Nothing records that.

## 3.6.1 Rate-limited by design

One event **per inode per cache population**. A corrupt descriptor read
a thousand times during one mount's life produces one event, not a
thousand.

This is deliberate: a filesystem with many corrupt descriptors would
otherwise drown the audit stream at exactly the moment the stream is
most needed. The consequence is that event count says nothing about
access count — one event does not mean one access.

## 3.6.2 Informational only

The kernel denies the access regardless. A corrupt descriptor fails
closed, and this event is not part of that decision — it is what tells
an administrator the corruption exists at all.

Without it, a file with a broken descriptor is simply inaccessible, with
nothing anywhere explaining why.

## 3.6.3 Events that do not exist

Three absences in the KACS set are worth stating, because each is a
reasonable thing to look for:

- **No `logon-session-created`.** Sessions are announced only when they
  end (§3.5). Track creation through authd's own records or by polling
  `/sys/kernel/security/kacs/sessions`.
- **No `token-created`.** A token's existence is observable through
  process inspection, not through an event.
- **No periodic or heartbeat events.** Audit here is entirely
  event-driven. A silent stream means nothing happened, not that
  anything is broken.

These are intentional. The kernel's audit surface covers access
decisions and session endings; other lifecycle tracking belongs to the
layers above it.

---

# 4.1 STRATAFS_COPY_UP

_Peios / Using Peios / Events / Filesystem Events_

> The event fired on every StrataFS copy-up, successful or not — six keys, no caller identity, and where ENOTDIR actually surfaces.

Fires on every StrataFS copy-up, successful or not.

Event type string: `STRATAFS_COPY_UP`.

| Key | Meaning |
|---|---|
| `path` | The relative path within the mount, `/`-prefixed. |
| `provider_index` | The stratum the object was copied from. |
| `provider_stratum` | That stratum's path. |
| `create_index` | The stratum it was copied into. |
| `create_stratum` | That stratum's path. |
| `result_errno` | Zero on success, the failure otherwise. |

Six keys, and no seventh.

## 4.1.1 No caller in the payload

Nothing here names a token, and that is not an omission.

Copy-up preserves the source object's descriptor, so the resulting file
records nothing about who caused it to exist. The caller is recovered
from the **envelope** instead: KMES stamps the effective, true and
process token GUIDs onto the header at ring-write time, and because
copy-up runs in the caller's own context, those are the caller's
(§1.2).

A consumer reading only payloads will conclude these events are
anonymous. They are not — the identity is one level out.

## 4.1.2 ENOTDIR arrives here, not in the refusal event

A parent materialisation that fails with `ENOTDIR` is reported through
this event, carried in `result_errno`, rather than through
`STRATAFS_MUTATION_REFUSED` (§4.2).

That is worth knowing when searching for a refusal and finding nothing:
the record exists, under a different type, with all the required fields
present.

---

# 4.2 STRATAFS_MUTATION_REFUSED

_Peios / Using Peios / Events / Filesystem Events_

> Refusals that come from how a mount is arranged rather than from an access check — what counts, two irregularities, and what is not audited at all.

Fires when a mutation is refused because of how the mount is arranged,
rather than because of an access check.

Event type string: `STRATAFS_MUTATION_REFUSED`.

| Key | Meaning |
|---|---|
| `path` | The relative path within the mount. |
| `operation` | The operation name. |
| `provider_index` | The provider stratum's index. |
| `provider_stratum` | That stratum's path. |
| `errno` | The refusal. |
| deferred flag | Whether the refusal was deferred. |

## 4.2.1 What counts as an arrangement refusal

One explicit list, matching the specification's enumeration exactly:
`EROFS`, `EXDEV`, `ENOTDIR`, `EISDIR`, `ENOTEMPTY`, `EEXIST`, `EINVAL`.

Call sites cover every mutating path — writes, mappings, truncation,
`fallocate`, splice, `copy_file_range`, `remap_file_range`, `setattr`,
`setxattr`, `removexattr`, creation, tmpfile, unlink, rmdir, link,
supersede and rename.

**`EACCES` is deliberately absent.** A refusal produced by an access
check is audited by the mechanism that performed it, and StrataFS does
not duplicate those records. Looking here for a permission denial finds
nothing; look for `access-audit` (§3.1) instead.

## 4.2.2 What these records are for

They report a mismatch between what a caller attempted and how the mount
is arranged — software writing where it cannot, or an arrangement that
does not admit an operation someone expected.

That is diagnostic information about configuration, and it is otherwise
visible only as an error returned to a caller that may well discard it.

Rollbacks are audited under this same type with the deferred flag set: a
create or link whose outer bookkeeping failed and whose lower object
could not be removed again, and a failed publication rollback after a
copy-up.

## 4.2.3 Two irregularities

**A refusal raised before a provider is known** — creation, tmpfile, the
heads of link and rename — passes a provider index of `-1`, so
`provider_stratum` is emitted as an empty string. The specification asks
for the provider stratum in every refusal record, so this is a gap
rather than a design.

**A refused deferred deletion is audited on any non-zero result**, not
only on the arrangement errors, so one refused by an access check does
produce a StrataFS record. This is a deliberate exception to the
`EACCES` exclusion above: the requirement to audit a deferred deletion
is unconditional, because by then nobody is left to receive the error.

## 4.2.4 What is not audited

Resolution, revalidation and enumeration emit nothing. They occur on
every path operation, reveal nothing the resulting access check does
not, and recording them would produce volume out of all proportion to
their significance. There is no audit call anywhere in the lookup path.

Access checks against provider objects are audited by KACS under its own
rules.

---

# 5.1 Registry Events

_Peios / Using Peios / Events / Registry Events_

> The seven audit events LCS emits, which carry the caller summary, and which are audited unconditionally.

LCS emits **seven** audit events through KMES. Six of them carry the
caller summary (§2.3) rather than the subject record.

| Event | Emitted when |
|---|---|
| `LCS_KEY_OPEN_AUDIT` | A key open matched a SACL audit ACE. |
| `LCS_BACKUP_START` | Before `REG_IOC_BACKUP` reads any subtree data. |
| `LCS_BACKUP_COMPLETE` | After a backup completes, or fails after starting. |
| `LCS_RESTORE_START` | Before `REG_IOC_RESTORE` modifies any source state. |
| `LCS_RESTORE_COMPLETE` | After a restore completes, or fails after starting. |
| `LCS_SOURCE_VALIDATION_FAILURE` | LCS rejected malformed source data. |
| `LCS_SELF_CONFIG_INVALID` | LCS rejected an invalid self-configuration value. |

Every payload is a msgpack map with string keys. GUIDs are 16-byte
binary values; SIDs are binary KACS encodings.

## 5.1.1 Backup and restore are audited unconditionally

Whatever the SACL on the target key says. They are privilege-gated bulk
operations that bypass per-key access checks entirely, so the audit
trail is the only record that they happened at all.

The start/complete pairing is deliberate too. `LCS_BACKUP_START` is
emitted *before* any data is read and `LCS_RESTORE_START` before any
state is modified, so an operation that dies partway still leaves
evidence that it began.

## 5.1.2 Separately: watch records

LCS also produces **watch records**, read from a key file descriptor.
These are not KMES events, not msgpack, and not audit — see §5.4.

---

# 5.2 LCS_KEY_OPEN_AUDIT

_Peios / Using Peios / Events / Registry Events_

> The event fired when a key open matches a SACL audit ACE, the SACL that produces nothing, and what happens when emission fails.

Fires when a key open matched a SACL audit ACE.

| Key | Meaning |
|---|---|
| `caller` | Caller summary (§2.3). |
| key GUID | The key that was opened. |
| `requested_access` | The mask after registry generic mapping, with `MAXIMUM_ALLOWED` re-added if the caller asked for it. |
| `granted_access` | The mask granted. Forced to zero on a denial. |
| decision | `allowed` or `denied`. |
| `sacl_match_flags` | Bit 0 for a success-audit match, bit 1 for a failure-audit match. No other bits. |

`granted_access` being zero on a denial is **enforced**, not merely
intended: a denied event carrying a non-zero granted mask is rejected as
a malformed payload.

SACL evaluation follows the KACS AccessCheck algorithm — the SACL is
evaluated alongside the DACL, not separately. Reading or modifying a
SACL requires `ACCESS_SYSTEM_SECURITY`, itself gated by
`SeSecurityPrivilege`.

## 5.2.1 One matching SACL that produces nothing

A request of `MAXIMUM_ALLOWED` **alone** maps to a desired mask of zero.
AccessCheck's SACL walk tests each audit ACE's mask against the mapped
desired access, and no ACE matches zero.

So an open with a matching audit ACE emits no event, and LCS emits
nothing. This is a real hole in coverage for anyone auditing key opens:
the one request shape that asks for everything is the one that records
nothing.

## 5.2.2 When emission fails

The policy is specific to this event, and differs from the bulk ones.

If LCS cannot **construct** a valid payload — corrupt internal state,
allocation failure, anything on the LCS side — the open fails with
`EIO` and no key fd is published. The audit is a precondition of the
access.

If the payload is valid but KMES cannot **retain** it — unavailable,
ring drops, capacity pressure, no consumer — the access decision and the
fd publication are unaffected. Loss accounting is KMES's problem, and
shows up as a `synthetic.gap` (§8.1) rather than as a failed open.

---

# 5.3 Validation and Configuration Failures

_Peios / Using Peios / Events / Registry Events_

> The two events LCS emits for malformed source data and invalid self-configuration — and why a first boot produces nineteen of them.

## 5.3.1 LCS_SOURCE_VALIDATION_FAILURE

Fires when LCS rejects malformed source data.

Carries the source slot identifier, then — where each is known — the
hive name, the RSI request id, the operation code and the key GUID. The
last field, `validation_class`, names what was wrong.

There are **twelve** classes:

| Group | Classes |
|---|---|
| Name fields | `malformed_layer_name`, `malformed_key_name`, `malformed_value_name` |
| Structural | `malformed_response_payload`, `malformed_key_metadata`, `malformed_value_payload`, `malformed_delete_layer_orphan_list` |
| Descriptors | `malformed_security_descriptor`, `malformed_layer_metadata_security_descriptor` |
| Sequencing | `future_sequence_number`, `duplicate_winning_sequence_tie` |
| Protocol | `unknown_rsi_status_code` |

The three name classes are field-specific — layer-name fields, key
component or child-name fields, and value-name fields respectively.

The structural classes cover a response whose operation-specific payload
has the wrong shape or trailing bytes; a lookup or enumeration whose
metadata block is incomplete, duplicated, unreferenced or nil; a value
payload with an invalid type, a tombstone/data mismatch or oversized
data; and an invalid orphan GUID array from `RSI_DELETE_LAYER`.

## 5.3.2 LCS_SELF_CONFIG_INVALID

Fires when LCS rejects an invalid self-configuration value.

Carries the parent path and value name of the offending parameter, the
expected type and numeric range, what was actually received — one of
`missing`, `wrong_type` or `dword_out_of_range`, with the actual type or
value where applicable — and the value LCS retained instead.

### 5.3.2.1 Expect nineteen of these on a first boot

Because `missing` counts as invalid, a first boot before seed restore
emits one event **per parameter on each refresh**: nineteen events
against an empty `Registry\` key.

That is correct and expected, and it is a noticeable share of the boot
audit stream. A consumer alerting on validation failures needs to know
it, or every fresh machine looks like it is failing.

---

# 5.4 Watch Records

_Peios / Using Peios / Events / Registry Events_

> Watch records are not KMES events — binary records read from a key file descriptor, their layout, and what a subtree watch adds.

Watch records are **not KMES events**. They are binary records read from
a key file descriptor with `read()`, and they exist to tell a watcher
that something under a key changed.

They are documented here because an operator asking what the registry
reports would otherwise miss them, but nothing else in this book applies
to them: no msgpack, no envelope, no identity stamps, no audit meaning.

## 5.4.1 Reading

A single `read()` returns as many complete records as fit in the
caller's buffer. A record is never split across two calls.

If the buffer cannot hold even the first queued record, `read()` fails
with `EINVAL` — the buffer is too small to make progress, and the caller
retries with a larger one. On an armed fd with an empty queue, `read()`
blocks, or returns `EAGAIN` under `O_NONBLOCK`.

Only records copied out in full are dequeued.

## 5.4.2 Layout

Every record begins with the same four fields.

| Offset | Size | Field |
|---|---|---|
| 0 | 4 | `total_len` |
| 4 | 2 | `event_type` |
| 6 | 2 | `name_len` |
| 8 | `name_len` | `name`, UTF-8 |

All integers are little-endian. `total_len` is the whole record
including this header, it is how a consumer advances to the next record,
and it is the **only** safe way to do so.

## 5.4.3 Subtree watches carry a path

A subtree watch's records carry additional fields after the name,
locating the key the change happened on relative to the watched key.

| Size | Field |
|---|---|
| 2 | `path_depth` |
| `2 + n` each | `path_components`: a `u16` length, then that many UTF-8 bytes |

`path_depth` is the number of components from the watched key down to
the changed key. Zero means the change was on the watched key itself.

The components are length-prefixed rather than joined by a separator
because registry names can contain any Unicode character and value names
can contain backslashes. A concatenated path string would be ambiguous.

`OVERFLOW` records are emitted in the bare eight-byte form, with no path
even on a subtree watch — there is no single key to name when the queue
itself overflowed.

The header offsets are ABI, named in `uapi/pkm/lcs.h` and listed in the
Peios Kernel TRM §5.A.

---

# 6.1 Job Events

_Peios / Using Peios / Events / Service Events_

> The three events peinit emits across a job's lifecycle, their payloads, and the ordering guarantee between them.

peinit emits a structured event at every job and operation lifecycle
transition. All of them go into the KMES ring buffer, encoded as msgpack
per the envelope in §1.2.

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

## 6.1.1 The three job events

| Event | Fires when | Carries |
|---|---|---|
| `job.created` | The job object exists. | Job identifier, service name, type, image path, identity, operation identifier. |
| `job.started` | `exec` succeeded. | Job identifier, PID, cgroup path. |
| `job.ended` | The process exited or was killed. | Job identifier, final state, exit code or signal, duration, failure cause. |

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

## 6.1.2 Ordering

When one runtime step produces several lifecycle events, peinit emits
them in causal order **before** committing the retained state for that
step. A consumer sees the events in an order consistent with what
happened, not in whatever order the writes completed.

---

# 6.2 Operation Events

_Peios / Using Peios / Events / Service Events_

> The seven operation lifecycle events, the five fields they share, what duration_ns measures from, and the one field that goes by three names.

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

Seven types, each adding its own fields:

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

## 6.2.1 duration_ns measures from creation

Not from the start of execution. The reasoning is the same as for the
operation timeout: what a caller waited is what matters, and queue time
is part of it.

An operation that sat in a queue for a minute and then ran for a second
reports 61 seconds, not 1.

## 6.2.2 One field, three names

The same value appears under three spellings across two surfaces:

- `failure_reason` on `operation.failed`
- `reason` on `operation.cancelled` and `operation.aborted`
- `error` in the control interface's operation view (PSPU §4)

A consumer correlating an event stream against the control interface has
to map all three onto each other.

---

# 7.1 The Event Set

_Peios / Using Peios / Events / Package Events_

> The eleven event types peipkg emits, their payload fields, and the privilege emission depends on.

peipkg emits eleven event types into the kernel event subsystem. The
caller's identity is stamped into the envelope by the kernel (§1.2) and
is not part of the payload.

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

`peipkg.config-change` is specified and **not emitted**. A trust-policy
or transport-flag change today produces no event at all.

## 7.1.1 Payload fields

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

Note `timestamp` here is an **RFC 3339 string**, not the uint used
elsewhere in this book (§1.3). peipkg carries its own.

## 7.1.2 Emission depends on a privilege

An audit privilege on the caller's token. Without it, emission fails,
peipkg warns, and **the operation proceeds unaudited**. The absence of
an event does not mean the operation did not happen.

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

---

# 7.2 What Is Not Recorded

_Peios / Using Peios / Events / Package Events_

> The specific gaps in peipkg's audit trail — each a place where something happened and nothing was written — and how to read the trail knowing them.

The gaps in peipkg's audit trail are specific, and each one is a place
where something happened and nothing was written.

- An **install or upgrade event carries no source repository**, although
  one is known at the time.
- A **committed cross-root operation's success event carries no
  transaction identifier**, so it cannot be correlated with the rest of
  its transaction.
- **Automatic recovery** at the head of an ordinary operation emits
  nothing. Only recovery through `peipkg recover` produces
  `peipkg.recovery`.
- **`peipkg recover`'s failure paths** emit nothing. A recovery that
  fails leaves no record that it was attempted.
- **Declining at a prompt** emits nothing.
- **Enabling insecure transport** emits no authorisation record, and so
  does **installing unsigned content under an `optional` policy** — the
  two decisions most worth recording are the two that are not.
- **`peipkg-compose` emits nothing at all.** Image composition is
  entirely unaudited.

## 7.2.1 Reading the trail with these in mind

Two consequences follow for anyone building on this stream.

**Absence is not evidence.** An operation with no event may have been
performed by a caller without the audit privilege, may have taken one of
the paths above, or may not have happened. The three are
indistinguishable from the stream.

**Transaction correlation is incomplete.** `txn_id` ties an operation's
events together, but the cross-root success case omits it, so a
reconstruction keyed on `txn_id` will silently drop those.

---

# 8.1 Synthetic Events

_Peios / Using Peios / Events / Event Daemon Events_

> The records eventd generates about itself, written straight to a shard and never through KMES — which shard, what the timestamp means, and what has no event at all.

Synthetic events are records eventd generates **about itself**. They are
written straight into a shard database and never touch KMES.

They carry no KMES header: no identity stamps, no sequence number, no
origin class. What they have is a wall-clock timestamp taken when eventd
generated the record, and a type string prefixed `synthetic.`, which is
what distinguishes them in the `events` table — there is no separate
record-type column.

| Condition | Type |
|---|---|
| Lost events detected on a CPU | `synthetic.gap` |
| eventd started and attached to KMES | `synthetic.startup` |
| Graceful shutdown beginning | `synthetic.shutdown` |
| A write to any store failed | `synthetic.storage_error` |
| A configuration value changed at runtime | `synthetic.config_change` |

Five, and no more. Payload schemas are in the eventd manual §3.2.

## 8.1.1 There is no event for bad input

Deliberately. Log and metric datagrams arrive unauthenticated from
arbitrary local processes, and emitting a durable record per bad
datagram would hand every process an amplification primitive
(PSPU §3.4).

These five are conditions eventd observed **about itself**, not
reactions to what it was sent.

## 8.1.2 Which shard they land in

**CPU-specific** — `synthetic.gap` — goes to the shard assigned to the
CPU that generated it, handed to that writer thread alongside that CPU's
ordinary events. A gap record travels with the events it describes.

**Daemon-wide** — startup, shutdown, config changes, storage errors — go
to shard 0 when shard 0 is writable, otherwise to the lowest-numbered
writable active shard. If no shard is writable, the event is skipped and
the failure is logged to standard error.

A storage error is why the fallback exists. It describes a failure on
one shard but is itself a daemon-wide notification, so it is not written
to the failing shard unless that shard has since been replaced and is
writable again. Writing the record of a shard's failure into that shard
would lose it exactly when it matters.

## 8.1.3 The timestamp is when eventd noticed

Not when the condition occurred.

A `synthetic.gap` is stamped at **detection**, which may be long after
the events it describes were overwritten — and after a restart it may be
the first thing written in a new boot about events lost in the previous
one.

An investigation that treats a gap record's timestamp as the time of the
loss will look in the wrong window.

## 8.1.4 Storage and ordering

Synthetic events live in the same shard databases as KMES events and
take part in the same batching, retention and queries. Access control
treats their types like any other, so
`Machine\System\eventd\Security\Events\synthetic` governs them.

They are ordered by their eventd-assigned timestamp and take no part in
per-CPU sequence numbering.

---

# Appendix A All Event Types

_Peios / Using Peios / Events_

> Every event type in this book in one table — thirty-four types across six emitters, plus the registry's watch records and the known holes.

Every event type in this book, in one table. Thirty-four types across
six emitters, plus the registry's watch records, which are a separate
mechanism.

## A.1 KMES events

| Type | Emitter | Where |
|---|---|---|
| `access-audit` | KACS | §3.1 |
| `continuous-audit` | KACS | §3.2 |
| `privilege-use` | KACS | §3.3 |
| `caap-policy-diagnostic` | KACS | §3.4 |
| `logon-session-destroyed` | KACS | §3.5 |
| `corrupt-sd` | KACS | §3.6 |
| `STRATAFS_COPY_UP` | StrataFS | §4.1 |
| `STRATAFS_MUTATION_REFUSED` | StrataFS | §4.2 |
| `LCS_KEY_OPEN_AUDIT` | LCS | §5.2 |
| `LCS_BACKUP_START` | LCS | §5.1 |
| `LCS_BACKUP_COMPLETE` | LCS | §5.1 |
| `LCS_RESTORE_START` | LCS | §5.1 |
| `LCS_RESTORE_COMPLETE` | LCS | §5.1 |
| `LCS_SOURCE_VALIDATION_FAILURE` | LCS | §5.3 |
| `LCS_SELF_CONFIG_INVALID` | LCS | §5.3 |
| `job.created` | peinit | §6.1 |
| `job.started` | peinit | §6.1 |
| `job.ended` | peinit | §6.1 |
| `operation.requested` | peinit | §6.2 |
| `operation.started` | peinit | §6.2 |
| `operation.completed` | peinit | §6.2 |
| `operation.failed` | peinit | §6.2 |
| `operation.cancelled` | peinit | §6.2 |
| `operation.merged` | peinit | §6.2 |
| `operation.aborted` | peinit | §6.2 |
| `peipkg.install` | peipkg | §7.1 |
| `peipkg.upgrade` | peipkg | §7.1 |
| `peipkg.uninstall` | peipkg | §7.1 |
| `peipkg.refresh` | peipkg | §7.1 |
| `peipkg.transaction-failed` | peipkg | §7.1 |
| `peipkg.recovery` | peipkg | §7.1 |
| `peipkg.authorisation` | peipkg | §7.1 |
| `peipkg.repo-add` | peipkg | §7.1 |
| `peipkg.repo-remove` | peipkg | §7.1 |
| `peipkg.claim` | peipkg | §7.1 |
| `peipkg.config-change` | peipkg | §7.1 — **specified, not emitted** |

## A.2 Not KMES

| Type | Emitter | Transport | Where |
|---|---|---|---|
| `synthetic.gap` | eventd | Written direct to a shard | §8.1 |
| `synthetic.startup` | eventd | Written direct to a shard | §8.1 |
| `synthetic.shutdown` | eventd | Written direct to a shard | §8.1 |
| `synthetic.storage_error` | eventd | Written direct to a shard | §8.1 |
| `synthetic.config_change` | eventd | Written direct to a shard | §8.1 |
| Watch records | LCS | `read()` on a key fd | §5.4 |

## A.3 Which carry an identity, and how

| Events | Identity from |
|---|---|
| KACS, all but `logon-session-destroyed` | `subject` record in the payload (§2.1) |
| `logon-session-destroyed` | `user_sid` and `session_id` directly; the session was the subject |
| LCS, six of seven | `caller` summary in the payload (§2.3) |
| StrataFS, peinit, peipkg | The **envelope** only. No identity in the payload (§1.2) |
| eventd synthetic | None. eventd is describing itself |

## A.4 Known holes

Collected from the chapters, because a reader planning coverage needs
them in one place:

- `peipkg.config-change` is never emitted (§7.1).
- peipkg omits the source repository on install and upgrade, the
  transaction id on committed cross-root success, and emits nothing for
  automatic recovery, recovery failures, declined prompts, insecure
  transport, unsigned installs under an `optional` policy, or
  `peipkg-compose` (§7.2).
- A registry key open requesting `MAXIMUM_ALLOWED` alone emits no
  `LCS_KEY_OPEN_AUDIT`, because the mapped desired mask is zero and no
  ACE matches zero (§5.2).
- StrataFS refusals raised before a provider is known emit an empty
  `provider_stratum` (§4.2).
- `caap-policy-diagnostic` with `kind = staging-mismatch` does not
  identify which rule differed, and its two masks can be equal while
  `object_results_differ` is true (§3.4).
- There is no `logon-session-created`, no `token-created`, and no
  heartbeat (§3.6).
- peipkg emits nothing when the caller's token lacks the audit privilege
  (§7.1).
