MSIP daemons
Single-page view · as markdown
Presentation hints
Peios / Developing for Peios / MSIP daemons
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 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
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.
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.
These are daemon startup effects, including warning-only socket security. 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:
The device is deliberately invented, not a real target to substitute later. Omitted fields have schema defaults. 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.
2. Start only the lab daemon #
In the first terminal, with LAB set and verified:
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:
The last action is intentional: successful job execution produces a finish
page, not an ended conversation. The dry-run executor's restart
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 and client, not captured test output:
bound:reports a conversation ID andattached=false.- Turns advance through
mode,disk.choose,confirm,install.progress, andinstall.done. The client waits on the progress turn. act.rebootproduces== end: Complete Restarting the machine.and the client exits with status0for 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.
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
and disconnect and Start handling.
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
and same-turn rejection/patch handling.
Keep the evidence in scope #
- Protect inventory, console output, and logs. Do not add credentials or
first-boot account setup to this exercise.
--setvalues 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 and log rendering. - The checked-in VM queue-inspection script
omits
act.rebootand 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 exits1. 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.