User-scoped units (hq ADR 0177, to-be 38 WP6), with lingering and a manager that is not running #91

Merged
mesh-admin merged 2 commits from feat/user-scoped-units into main 2026-10-04 10:43:18 +00:00
Contributor

PR #72, rebased onto main, with the review gaps closed:

  • linger on the user shape: recorded once and given back like the shell (#89); refused on OpenRC.
  • A user unit whose account's manager is not running is "waiting", applied by the next apply after a login or with linger. The manager is never asked while it is down, because --machine would start it.
  • Removal with the manager down, or a bus error, waits and retries; it is never fatal.
  • meshMadeUnits / installedByHand know ~/.config/systemd/user and /etc/systemd/user; records are keyed by scope and name.
  • A unit moved between scopes gives the old one back.

It supersedes #72. go build, vet and test (-count=1) pass; gofmt is clean.

PR #72, rebased onto main, with the review gaps closed: - **`linger`** on the `user` shape: recorded once and given back like the shell (#89); refused on OpenRC. - **A user unit whose account's manager is not running** is "waiting", applied by the next apply after a login or with linger. The manager is never asked while it is down, because `--machine` would start it. - **Removal with the manager down,** or a bus error, waits and retries; it is never fatal. - **`meshMadeUnits` / `installedByHand`** know `~/.config/systemd/user` and `/etc/systemd/user`; records are keyed by scope and name. - **A unit moved between scopes** gives the old one back. It supersedes #72. go build, vet and test (`-count=1`) pass; gofmt is clean.
mesh-admin added 2 commits 2026-10-04 10:43:12 +00:00
A workstation's per-user daemons — a window manager's reload watcher, an
audio mask, a memory guard — are units in the operator account's own service
manager, and until now had no form the mesh could send (to-be 29). The
`service` shape gains `scope` ("system", the default, or "user") and `user`
(the account, named ${machine:account} by a module); a user-scoped unit
without an account, or a system unit naming one, is refused at parse.

The host reaches the account's manager as `systemctl --user --machine=<account>@`
from its own process: no environment to forge, no user to switch to. Done on
the runner rather than per system, since every system's reading of a unit
already goes through systemctl. Apply, reflect-only and removal all go through
the same manager, and the applied record carries scope and user so removal
gives the unit back to the manager it came from. It answers only while that
manager runs — a login, or lingering enabled for the account; declaring
lingering is a follow-up.

Tests: a user-scoped unit is started and enabled in the account's manager and
recorded with its scope; a system unit never sees --user; the validation of
scope and user.
An account's manager runs only while it is logged in or lingers. A user-scoped unit
whose manager is not running is now "waiting" rather than failed, its record kept as
it was; its removal is never fatal (kept recorded, retried) and an account that is
gone is forgotten. Whether the manager runs is asked of user@<uid>.service in the
machine's manager: asking the account's own, through --machine, logs it in.

The user shape gains `linger`, set with loginctl, read back from logind's record,
and given back on removal like the shell. Unit files the mesh writes under
~/.config/systemd/user or /etc/systemd/user make that unit the mesh's, and made,
holds and found units are keyed by manager and name, so an account's unit and the
machine's of one name are two units. A service moved between managers gives the old
one back through the manager it was in. OpenRC refuses both.
mesh-admin merged commit 429672b7ff into main 2026-10-04 10:43:18 +00:00
mesh-admin deleted branch feat/user-scoped-units 2026-10-04 10:43:18 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-host#91