# MSIP daemons

---

# Presentation hints

_Peios / Developing for Peios / MSIP daemons_

> What a daemon can say about a page beyond its elements — the class vocabulary and element state the stock terminal surface honours, and what it does whether you hint or not.

MSIP keeps *what a page contains* apart from *how it is drawn*. A daemon sends a turn of typed elements; a surface draws them however it draws that type. That split is what lets one surface serve every daemon and one daemon be driven by any surface, including a script.

It also means a daemon cannot lay a page out. What it can do is say what a page *is for*, and let the surface decide what that looks like. PGSS §3.7 gives a turn a `class` and §3.8 gives an element one — "presentation hints", which the specification is careful to say promise nothing. This page is the vocabulary the stock terminal surface (`msip-tui`, the renderer behind `install-tui` and `oobe-tui`) actually honours, so that a hint you send lands somewhere.

A class a surface does not know is ignored, by design. Sending one costs nothing and breaks nothing; it just does not draw anything either.

## Turn classes

| Class | What the surface does |
|---|---|
| *(none)* | A form: heading, elements stacked down a column, the actions in a bar beneath. Right for most pages. |
| `menu` | The page is a choice among its actions. They are laid down the body as a list, one highlighted, rather than in a bar. For a page whose only elements are actions and a sentence — a mode chooser, a repair menu. |
| `confirm` | The page asks whether you mean it. The heading is marked, and the destructive action is drawn as such even when not focused. For the page before something irreversible. |
| `progress` | A job is running. The page's `progress` elements are drawn as a phase list — done, active with a bar, pending — and its `log` fills whatever height remains. |

One class per turn is the expectation; the surface reads each independently, so nothing stops you sending two, but nothing today means anything by the combination.

## Element state the surface reads

These are defined by the [element types](/peios/advanced-peios/pgss/msip/element-types.md) appendix rather than being surface conventions, but what the surface makes of them is worth knowing when you write a page.

| On | State | Drawn as |
|---|---|---|
| `action` | `primary` | The accent colour and bold. Also what an unattended answerer presses, so there is at most one. |
| `action` | `destructive` | Red, and red-highlighted when focused. Pressing it is not otherwise made harder: the `confirm` page is where that happens. |
| `string` | `secret` | Masked; never echoed, logged, or carried in an update. |
| `string` | `placeholder` | Ghost text in an empty, unfocused field. |
| `table` | `align: right` on a column | Right-aligned cells. Sizes and counts read better that way. |
| `table` | `enabled: false` on a row | Listed, dimmed, skipped by the highlight, refused as an answer. |
| `table` | `note` on a row | A short remark after the cells — typically why the row is disabled. |
| `table` | `empty` | Shown in place of the rows when there are none. |
| any | `help` | See below. |
| any | `error` | Red, under the element, until an update clears it. |

## What it does whether you hint or not

Some of the surface's behaviour is not a hint you can send but a fact to design pages around.

**Disabled elements take focus.** Tab reaches them, and while one is focused the footer shows its `help`. This is how the surface treats a thing you show greyed out: as something the person is meant to *see* and ask about, not something to hide. So an element you disable should carry a `help` that says why — "no keymaps are packaged yet", "domain membership needs a directory source" — because that sentence is what the person will read.

**Focus opens on the first element that can be answered.** A page that starts with two disabled fields opens on the first enabled one, or the primary action.

**A field's note line is reserved.** An element with `help` or an `error` always has a line under it, so focus moving does not move the page. Help is shown there while the element is focused; an error is shown there always.

**A table drops columns from the right** when it has no room for them all, as the appendix asks. Put the column a person must see first.

**Leaving is the surface's sentence, not yours.** Esc opens a question whose wording comes from the surface binary, because what leaving means — an installation carrying on without you, a machine left without an account — is a fact about the program, not the page.

**A script sees none of this.** `msip-drive` prints elements and answers them from its arguments; every hint above is invisible to it, which is the point of hints. A page has to work with none of them honoured.

---

# Rehearse the headless installer in a lab

_Peios / Developing for Peios / MSIP daemons_

> Exercise an installerd conversation with msip-drive, a private socket, and a synthetic inventory, without installing a real disk.

Use this exercise to understand how `msip-drive` answers an `installerd`
conversation. It simulates an installation against an invented disk. For an
actual installation, use [Installing to disk](/peios/using-peios/disks-and-filesystems/installing-to-disk.md).

> [!IMPORTANT]
> This is a source-derived rehearsal for installer commit
> [`0b687372ad41132668824216da51558bbfe53c01`](https://github.com/peios/installer/tree/0b687372ad41132668824216da51558bbfe53c01).
> The commands below have not been run to validate this guide against an image
> or released binaries. They are not a validated unattended-install recipe.

## 1. Prepare a private, disposable lab

Use a disposable lab instance with the matching `installerd` and `msip-drive`
binaries and an account allowed to use its lab socket. Create a **new private
directory**, restrict access using the lab system's access controls, and verify
that protection before starting. In each terminal, set `LAB` to that directory's
absolute path. The examples do not create or protect the directory for you.
Keep any saved output there and use only synthetic, nonsensitive data.

> [!WARNING]
> Never use the default `/run/installerd.sock`, a production socket, or an old
> lab socket. Even with `--dry-run`, daemon startup attempts to remove the
> supplied socket path, binds a socket there, and calls `sd set` to apply its
> descriptor. Descriptor failures only warn; they do not stop startup. A wrong
> path can disrupt access to an existing service. Inspect warnings and verify
> the private directory's protection; do not infer protection from a listening
> message alone.

These are [daemon startup effects](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/installerd/src/main.rs#L93-L105),
including [warning-only socket security](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/msip-serve/src/lib.rs#L649-L685).
Dry-run does not partition, format, or copy to the target disk; it is not a
side-effect-free or fully isolated mode.

Save this JSON as `inventory.json` in that directory:

```json
{"disks":[{"device":"/dev/msip-lab-only","model":"Synthetic MSIP lab disk","size":8589934592}],"controllers":[]}
```

The device is deliberately invented, not a real target to substitute later.
Omitted fields have [schema defaults](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/installerd/src/executor.rs#L18-L42).
Always supply the inventory: without it, dry-run still probes this machine's
block devices and controllers. With it, the executor uses the supplied
[synthetic inventory](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/installerd/src/executor.rs#L325-L402).

## 2. Start only the lab daemon

In the first terminal, with `LAB` set and verified:

```sh
installerd --dry-run --socket "${LAB:?Set LAB to the new private directory}/installerd.sock" \
  --inventory "$LAB/inventory.json"
```

Before starting a client, check that daemon output explicitly says
`installerd (dry run) listening on` followed by the exact private socket path.
Stop if the mode, path, or security checks are wrong. Leave the daemon running;
do not connect any other surface to it. Use a fresh directory and daemon for
each attempt rather than reusing an uncertain conversation.

## 3. Drive the synthetic conversation

In the second terminal, with `LAB` set to the same verified directory:

```sh
msip-drive --socket "${LAB:?Set LAB to the new private directory}/installerd.sock" --kind install \
  --set disk.target=/dev/msip-lab-only \
  --press act.install --press nav.next --press act.begin --press act.reboot
```

The last action is intentional: successful job execution produces a finish
page, not an ended conversation. The [dry-run executor's restart](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/installerd/src/executor.rs#L404-L452)
returns success **without rebooting the machine**. Do not remove `--dry-run`
or aim this sequence at a real service.

The expected sequence below is inferred from the
[flow](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/installerd/src/flow.rs#L621-L756)
and [client](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/msip-drive/src/main.rs#L141-L237),
not captured test output:

1. `bound:` reports a conversation ID and `attached=false`.
2. Turns advance through `mode`, `disk.choose`, `confirm`, `install.progress`,
   and `install.done`. The client waits on the progress turn.
3. `act.reboot` produces `== end: Complete Restarting the machine.` and the
   client exits with status `0` for that completed conversation.

`install.done` means the executor reported job success. `End Complete` means
the restart request was accepted or simulated; neither proves a restart or
successful disk boot. A real restart request is accepted when
[`svctl shutdown reboot` returns success](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/installerd/src/real.rs#L977-L1001).
In this lab, no disk installation or reboot takes place. Simulated percentages
do not verify partitioning, copying, image integrity, cleanup, or bootability.

## If it stalls or reconnects

**Do not blindly replay the command.** The client's `Start` implicitly joins
the daemon's one live, in-memory conversation and replays its current turn.
It has no conversation-ID guard and can consume a queued action immediately;
checking printed `attached=true` is not a barrier before an action runs.
If that appears in this fresh-lab exercise, stop the attempt and investigate
the unexpected state. The new private daemon and exclusive socket are the
precondition, not a check performed by the client.

Disconnecting the client **does not cancel the daemon's job**. A daemon restart
loses the in-memory conversation; a failed or completed conversation is also
cleared, so a later `Start` opens a new one. This is not durable recovery.
See [job lifetime](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/msip-serve/src/lib.rs#L239-L290)
and [disconnect and Start handling](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/msip-serve/src/lib.rs#L317-L508).

The driver consumes actions only on `NewTurn`, not same-turn `Updated` events.
A rescan or rejected input/reboot can therefore leave it waiting indefinitely,
even with more presses queued. It has no built-in timeout. An external watchdog
that stops the client neither cancels the job nor undoes changes. A client with
no presses is not a persistent observer: it exits with status `1` when an
actionable turn arrives. These limits follow from the
[client loop](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/msip-drive/src/main.rs#L141-L237)
and [same-turn rejection/patch handling](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/msip-serve/src/lib.rs#L558-L639).

## Keep the evidence in scope

- Protect inventory, console output, and logs. Do not add credentials or
  first-boot account setup to this exercise. `--set` values are command-line
  arguments; only daemon-marked secret elements are masked in the client trace.
  The client also prints only the last three stored log lines per update, so
  its output is not a unique, exhaustive audit journal. See
  [trace handling](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/msip-drive/src/main.rs#L191-L220)
  and [log rendering](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/msip-drive/src/main.rs#L258-L280).
- The checked-in [VM queue-inspection script](https://github.com/peios/installer/blob/0b687372ad41132668824216da51558bbfe53c01/tests/vm/install-and-check-queues.sh#L1-L45)
  omits `act.reboot` and logs, rather than asserts, the client's exit status.
  At this revision, a successful job reaches the actionable done turn with no
  presses left, so the client exits `1`. That script is not proof of a completed
  headless conversation; do not copy its real-device or mounting commands here.
- When finished, stop only the dedicated lab daemon. A real unattended-install
  guide still needs runtime validation of the shipped binaries, failure and
  reconnect handling, and independent checks of the installed disk and boot.
