ADR 0252: a module puts an account in a group, and the mesh says when a new login is needed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

Issue 247 left adding the operator's account to a daemon's group a sudo step by hand. Decide that
a module declares it with the user resource's groups, that the mesh gives back only what it added,
and that the node-engine says relogin needed; amend to-be 41 with WP6 and resolve the issue.
This commit is contained in:
jochen
2026-10-08 12:04:50 +02:00
parent bd14e1b021
commit 3449775cb6
4 changed files with 237 additions and 7 deletions
+12 -2
View File
@@ -172,7 +172,7 @@ every other domain has something to run on. **Recorded by** the controller and t
**Purpose.** To decide, send and apply what each node runs, and to say why.
**Recorded by** the controller's `inventory` database. **Upstream of** Provisioning, Connectivity, Data
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221.
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252.
**Uses:** node, machine, module, seat, setting (Module), manifest (Module).
- **assignment** — a module put on a node, with the settings that node gives it.
@@ -206,6 +206,11 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221.
against this account's home and owned by it
([ADR 0181](../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
Not a name a manifest carries.
- **account group** — a group of the machine's group database that a module puts an account in, by
declaring the account with that group and nothing else of it. Several modules may each add one to the
same account; the mesh takes back only an account group it added
([ADR 0252](../02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md)).
Never *membership*, which is the bus's word.
## Provisioning — one module serving another
@@ -320,11 +325,14 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
**Purpose.** To notice what is wrong with the mesh, say it once, repair what may be repaired unattended,
and record what was done by hand. **Recorded by** the controller's condition store.
**Upstream of** Change and delivery (the first-node gate) and Operator and conversation (a condition
becomes a message). **Decided by** ADR 0227, 0231, 0240.
becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252.
**Uses:** node, module, node-engine (Core).
- **health** — a module's or a core component's statement of how it is alive and ready, which the
node-engine judges ([ADR 0240](../02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md)).
- **relogin needed** — an account's health when the group database lists it in an account group and its
running session, started before, does not have that group yet. Said by the node-engine as the module's
resource of kind `account`; it clears at the first look after a new login (ADR 0252).
- **probe** — one look at one thing's health, run by whoever owns the verdict; what a probe returns is a
**finding**, before it becomes a condition.
- **signal** and **watchdog** — something that must keep happening (a heartbeat, a report after a send),
@@ -455,6 +463,8 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| | what a delivery would do to the mesh | **delivery plan** | Change and delivery |
| | the sending of one commit across nodes | **walk** (verb `plans`) | Change and delivery |
| | a step a person starts, like the bus's | **planned step** | Core |
| membership | the subjects a bus account is issued | **membership** | Provisioning |
| | an account in a group of the machine | **account group** | Placement |
| push | the controller giving nodes their declarations | **send** (verb `push`) | Placement |
| | a git push | **git push** | The record |
| | a phone notification | **push notification** | Operator and conversation |
@@ -0,0 +1,136 @@
---
topic: what runs on it
status: accepted
date: 2026-10-08
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
---
# 252. A module puts an account in a group, and the mesh says when a new login is needed
## Context
The module of a peripheral-lighting daemon installs the daemon, its kernel driver and its client
library. The driver's package makes a group and gives the devices' files to it. The daemon refuses to
start for an account outside that group: "User is not a member of the openrazer group". On both
workstations the operator's account was not in it, so the daemon failed at every start
([issue 247](../04-ISSUES/247-a-module-cannot-put-the-operators-account-in-a-group/00-report.md)). The fix
was a hand step with `sudo`, outside the mesh. Nothing recorded why the account was in the group, and
nothing would take it out again.
What the mesh already had, measured on 2026-10-08 on the main branches:
- **The `user` resource takes `groups`, additively.** The node-engine appends with `usermod --append`
and never removes a group. It never gives one back either, even when the resource that named it is
undeclared.
- **The controller already lets several modules declare one account**, since a change of 2026-10-04: a
module may add groups to an account another module declares; two modules that both set its shell or
its home are refused, naming both. Issue 247 and the lighting module's documentation, both written the
day after, still said a second module would be refused. Neither was true any more, and nobody used the
field.
- **A module's accounts are composed before everything else of the module** (issue 213), so that a file
owned by the account finds it made. An account declared only to be put in a group would then be put in
a group whose package is not yet installed. The first apply fails that resource; the second heals it.
- **A group takes effect at the next login.** The database changes at once. Every process already
running keeps the groups it started with: the account's own service manager, and the daemon that
manager starts. Nothing in the mesh said that a new login was the step left.
## Considered Options
1. **A new resource kind for one account group** (account, group), held by the module that needs it.
It would read well. Every node-engine already running parses its declaration strictly and refuses one
that names a kind it does not know, whole. So the kind could be used only once every machine that
runs the module had a new node-engine, and a module assigned early would stop its machine applying
anything. The `user` resource with `groups` alone says the same thing, and every node-engine already
applies it. Rejected.
2. **A contribution to the account's holder**, as ADR 0212 lets a module contribute to a seat. The
operator's account is not a seat, and a machine with no login shell module would have nowhere to
contribute to. Rejected.
3. **Keep it a hand step, and have the module's check say it.** That is the state issue 247 reports.
Rejected.
4. **The `user` resource with `groups` and nothing else of the account**, from any module; the mesh
takes back only what it added; and the node-engine says when a new login is needed. Chosen.
## Decision
1. **A module that needs an account in a group declares the account as a `user` resource with that
group, and nothing else of it.** For the operator's account the name is `${machine:account}`. The
word for such a group is **account group**. The shell and the home stay one module's per machine,
as before.
2. **Such a resource keeps its written place.** The controller composes a user resource that names
groups and no shell, home or lingering where the module wrote it, not first. A module writes it after
the package that makes the group. An account that says how it logs in or where it lives is still
composed first (issue 213).
3. **The node-engine takes back only what it gave.** It records on the resource every account group it
put the account in that the account was not in before. When the resource no longer asks for one, or
is undeclared, the account leaves that group. It does not leave it while another declared resource
still asks for the same group. A group the account was in before the mesh named it is never
recorded, and never taken back. The account is taken out of one group only (`gpasswd --delete`, or
busybox `delgroup`), read back, and a failure is said and is not fatal.
4. **A group the machine does not have fails that resource, saying so.** The node-engine asks the group
database before `usermod` runs, and the outcome names the package that should have made it.
5. **The node-engine says when a new login is needed.** The apply's outcome says that a session that
began before has the group only after a new login. On every look after that, the node-engine reads,
for each account a module put in account groups:
- whether the database lists the account in each group;
- whether the account's own service manager runs, read from the machine's own manager, which never
starts it;
- if it runs, the groups that process holds, from its status file.
It states the verdict as the module's resource of kind `account`. **Healthy** means the running
manager has every group, or nobody is logged in. **Unhealthy** says why: *not in the group*, or
*relogin needed: the account is in the group, and its running session began before it was; log out
of every session and in again, or reboot*. The controller raises it as the module's condition on two
statements in a row and clears it on the first healthy one, as it does every resource's health
(ADR 0240). The first-node gate reads it like any other: a module whose account needs a new login is
not yet good on that machine.
## Consequences
- The lighting module declares the operator's account in its group. On a workstation whose account is
not in it, the next send puts it there, and the module's condition says *relogin needed* until the
operator logs out and in again. Nobody runs `sudo` for it.
- **A new login holds a delivery of that module on that machine.** The first-node gate does not pass a
module whose account needs a new login, so a walk that starts on a workstation waits for the person.
The daemon cannot run before then, so a pass would be the false success ADR 0240 exists to prevent.
- A node-engine older than this one still puts the account in the group, since the field is the one it
already applies. It says nothing about the login, and it never takes the group back. A group it added
is not recorded, so a later node-engine never takes it back either.
- An account group two resources ask for is kept until the last one goes. If the one that added it goes
first, the other found it already there and never recorded it, so it stays with the account when that
one goes too. The mesh errs towards keeping a group, never towards taking one a module still needs.
- The account's other groups, and every group a person added by hand, are never changed.
## How it is checked
- **The node-engine** (`mesh-host`), in `internal/apply/groups_test.go`:
- an account group is added with `usermod --append` alone, recorded, and said with "new login";
- a group the account was in before is never recorded and never taken back;
- an undeclared resource gives back the group it added;
- a group another declared resource still asks for stays, and the outcome says which;
- a group no longer declared is given back while the account stays declared;
- a group the machine does not have fails the resource before `usermod` runs;
- a group a person removed is put back while it is declared.
- **The node-engine's account judge**, in `internal/accounts/accounts_test.go`: *relogin needed* when
the running manager lacks the group; healthy when it has it or none runs; *not in the group* when the
database does not list it; unknown, never healthy, when the database does not answer. It also checks
that every command the judge runs is a read.
- **The controller** (`mesh-controller`), in `internal/catalogue/shared_account_test.go`: an account
declared with groups alone is composed after the module's package, and one with a shell is still
composed first. The test beside it holds that two modules' groups on one account resolve together.
- **The catalogue** (`mesh-catalog`): the lighting module's manifest test holds its one account
resource, with that group alone and written after its packages.
- **Live**, once the operator's send has reached a workstation: openrazer's condition on that machine
says *relogin needed*; after a new login it clears, and `openrazer_check` answers `ok`.
## References
- [Issue 247](../04-ISSUES/247-a-module-cannot-put-the-operators-account-in-a-group/00-report.md)
- [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md) (the `user` resource gives back
the shell it set), [ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)
(the account's own manager, and why it is never asked directly),
[ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) (the operator
account), [ADR 0240](0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md) (the
node-engine judges health and the controller raises it)
- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md), WP6
@@ -2,8 +2,9 @@
layer: to-be
status: in-progress
code: [mesh-host, mesh-controller, mesh-catalog]
updated: 2026-10-04
updated: 2026-10-08
decisions:
- 02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md
- 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
@@ -83,6 +84,7 @@ WP2 the controller composes environment and shell code (mesh-controller)
WP3 the modules (mesh-catalog) needs WP2 to resolve
WP4 the service manager's module, finished (mesh-catalog) independent
WP5 assign and prove (operator-gated) needs WP1–WP3 merged and rolled
WP6 a module puts the account in a group (mesh-host, mesh-controller, mesh-catalog) issue 247
```
WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository
@@ -236,3 +238,57 @@ The follow-up records to-be 38 names are still owed:
- what a shell module assigned beside the holder does;
- how a person's own environment variable is a setting rather than a line, once issue 168 closes.
## WP6 — A module puts the account in a group
*`mesh-host`, `mesh-controller`, `mesh-catalog`. One day.
[Issue 247](../../04-ISSUES/247-a-module-cannot-put-the-operators-account-in-a-group/00-report.md),
[ADR 0252](../../02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md).*
The shell's module owns the account's shell. Another module may need the same account in an **account
group**: the lighting daemon's, the container runtime's, a serial port's. It declares the account as a
`user` resource naming that group and nothing else of it.
```
zsh user ${machine:account} shell /usr/bin/zsh composed first (issue 213)
openrazer package ×3 … then user ${machine:account} groups [openrazer] composed where written
│
node-engine usermod --append ── recorded on openrazer.account: "the mesh added openrazer"
│
account judge database lists it? ── running manager holds it? ── no: relogin needed
│ (module openrazer, kind account)
controller module.openrazer.<machine>.unhealthy, cleared on the first look after a new login
```
**What changes.**
- **The controller** composes a user resource that names groups and no shell, home or lingering where
its module wrote it, so after the package that makes the group. Any other account is still composed
first.
- **The node-engine, applying.** It asks the group database before adding, and a group the machine does
not have fails the resource with that reason. It adds with `usermod --append` alone. It records each
group it added that the account was not in before. It says in the outcome that a session that began
before has the group only after a new login.
- **The node-engine, removing.** A group this resource added and no longer asks for, or every such group
when the resource is undeclared, is given back. It is not given back while another declared resource
asks for it. The account is taken out of that one group (`gpasswd --delete`), read back, and a failure
is said and is not fatal, as giving back a shell is (WP1).
- **The node-engine, looking.** An account judge sits beside liveness and the failed-unit reading
(issue 315). On every look it reads, for each account a module puts in groups, the database's groups
and the groups of the account's running manager, from that process's status file. It states each as the
module's resource of kind `account`: healthy, or unhealthy with *not in the group* or *relogin needed*.
It only reads, and never asks the account's own manager, which asking would start (ADR 0177).
- **The lighting module** declares `${machine:account}` with the group `openrazer`, after its three
packages. Its check's finding for an account outside the group now says the module declares it, and
that a new login follows.
**Proof.** The tests ADR 0252 lists under *how it is checked*, in each of the three repositories. Live,
on the first workstation the operator sends the lighting module to:
1. the apply's outcome for `openrazer.account` says *put in openrazer*;
2. the module's condition on that machine says *relogin needed*;
3. after the operator logs out and in again, the condition clears and `openrazer_check` answers `ok`.
**Order.** The three repositories merge in any order. A node-engine older than WP6 already applies the
lighting module's resource: it puts the account in the group. It does not say the login, and does not
take the group back.
@@ -1,9 +1,10 @@
---
status: open
status: resolved
opened: 2026-10-05
located-in: []
fixed-by:
amended-design:
located-in: [mesh-host internal/apply (the user resource's groups), mesh-controller internal/catalogue (accountsFirst), mesh-catalog modules/openrazer]
fixed-by: [mesh-host PR 54, mesh-controller PR 139, mesh-catalog PR 125]
amended-design: 03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md
replay-none: not an incident of something that ran wrongly but a capability the manifest could not use; there is no earlier run to replay. The fix's own tests in each repository hold it (ADR 0252, how it is checked)
---
# 247 — A module cannot put the operator's account in a group
@@ -52,3 +53,30 @@ account in the group, says that a new login is needed, and leaves the account's
were. Unassigning it removes only a membership the module added.
Re-checked 2026-10-08: still holds — the node-engine on main has no resource for one group membership, and the lighting module declares none.
## Resolution — 2026-10-08
Decided in [ADR 0252](../../02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md),
built as [to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md) WP6.
**One statement above was already wrong when it was written.** Since 2026-10-04 the controller has let
a second module declare the account with groups only. It refuses only two modules that set the
account's shell or home. Nobody used that, because this report and the lighting module's documentation
both said it was refused. What was really missing:
- the account was composed before the module's packages, so on a fresh machine its group did not exist
yet;
- nothing recorded which groups the mesh added, so nothing could be given back;
- nothing said that a new login was the step left.
The answers to the open questions:
1. **Neither a new resource nor a contribution.** It is the `user` resource with `groups` and nothing
else of the account, which every node-engine already applies. A new resource kind would have been
refused, whole, by every node-engine older than it.
2. **A finding, stated as health.** The apply's outcome says a new login is needed. The node-engine's
account judge states the module's resource of kind `account` unhealthy with *relogin needed* until the
account's running service manager has the group, and the controller raises it as the module's
condition.
3. **Kept.** A group the account was in before any module named it is never recorded and never taken
back. Only a group the mesh added is given back, and not while another declared resource asks for it.