From 3449775cb60fb05b01be593d7af0e2b0eb62bdb3 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 12:04:50 +0200 Subject: [PATCH] ADR 0252: a module puts an account in a group, and the mesh says when a new login is needed 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. --- 00-META/glossary.md | 14 +- ...he-mesh-says-when-a-new-login-is-needed.md | 136 ++++++++++++++++++ ...-the-shell-and-the-accounts-environment.md | 58 +++++++- .../00-report.md | 36 ++++- 4 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md diff --git a/00-META/glossary.md b/00-META/glossary.md index abd85179..543a21d5 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -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 | diff --git a/02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md b/02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md new file mode 100644 index 00000000..37593058 --- /dev/null +++ b/02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md @@ -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 diff --git a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md index 6305e40c..901745b0 100644 --- a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md +++ b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md @@ -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..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. diff --git a/04-ISSUES/247-a-module-cannot-put-the-operators-account-in-a-group/00-report.md b/04-ISSUES/247-a-module-cannot-put-the-operators-account-in-a-group/00-report.md index fffa27c2..8451f060 100644 --- a/04-ISSUES/247-a-module-cannot-put-the-operators-account-in-a-group/00-report.md +++ b/04-ISSUES/247-a-module-cannot-put-the-operators-account-in-a-group/00-report.md @@ -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.