diff --git a/04-ISSUES/332-the-service-manager-cannot-clear-or-explain-a-failed-unit/00-report.md b/04-ISSUES/332-the-service-manager-cannot-clear-or-explain-a-failed-unit/00-report.md new file mode 100644 index 00000000..bfacc5fe --- /dev/null +++ b/04-ISSUES/332-the-service-manager-cannot-clear-or-explain-a-failed-unit/00-report.md @@ -0,0 +1,75 @@ +--- +status: located +opened: 2026-10-08 +located-in: [mesh-controller internal/catalogue (seats.go, the node-service-manager seat's verbs), mesh-catalog modules/systemd (the seat's holder)] +fixed-by: +amended-design: +--- + +# 332. The service manager cannot clear or explain a failed unit + +## Symptom + +On 2026-10-08 the workstation's machine finding (`machine..units`, from +[issue 315](../315-a-failed-unit-of-a-module-was-never-raised/00-report.md)) listed a user unit of a +clipboard tool that a module had replaced: load state `not-found`, active state `failed`, no unit file. +The operator approved removing it. Two steps were needed, and the `node-service-manager` seat had a verb +for neither: + +1. **Find what still asks for the unit.** The seat's `status` says the file is gone, and its `journal` + says when the unit last failed. Neither says which unit wants it, or whether an enable link in a + `*.wants` directory still names it and dangles. Issue 315's diagnosis found the link from the module's + README, not from a tool. +2. **Clear the failed record.** A manager keeps a unit's failed state and start-limit count until they + are reset. The seat had no verb for `systemctl reset-failed`, so the finding stays raised until the + account's manager restarts. + +Without either verb, the step left is a shell on the machine, which +[ADR 0245](../../02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md) rules out: the missing +tool is built in the module that owns the area, on its seat. + +## Fix + +Two verbs on the seat, each saying the command it replaces: + +- **`reset-failed {unit, scope}`** clears one unit's failed record. A unit is required, so the verb never + resets every failed unit at once. It replaces `systemctl reset-failed`. +- **`wanted-by {unit, scope}`** is read-only. It answers what wants, requires or triggers one unit: the + manager's reverse dependencies (`WantedBy`, `RequiredBy`, `UpheldBy`, `BoundBy`, `TriggeredBy` and the + rest, and the units that pull those in), and every enable link naming the unit in the scope's + configuration directories. In user scope that includes the account's own. Each link is given with where + it points, whether it dangles and whose directory it is in. It replaces `systemctl list-dependencies + --reverse` and reading the `*.wants` directories by hand. + +A third verb followed from the review of the first two. `systemctl disable` does not remove an enable +link once the unit's file is gone, so the link found by `wanted-by` had no verb to remove it either: + +- **`unlink-dangling {unit, scope, why}`** removes the dangling enable links named for one unit, only in + the account's or the machine's own configuration, on the person's word. It never removes a package's, a + generator's or the runtime's link, and leaves any link that changed since it was read. Each link is + recorded with its target and the reason before it is removed. + +The review also made every verb refuse a unit name holding `*`, `?` or `[`: the service manager expands a +glob to every unit it matches, so `reset-failed` or `stop` of `*` would have acted on all of them. + +They land in the three steps of +[ADR 0246](../../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md): + +1. The controller promises the verbs as optional: mesh-controller #158 (`reset-failed`, `wanted-by`) and + #160 (`unlink-dangling`). +2. The `systemd` module serves them: mesh-catalog #137 and #138. A module's check passes only against a + controller that already promises its verbs, so each merges after its step 1 is delivered. +3. The controller requires them, in a later pull request, once every holder serves them. + +## How it is checked + +- mesh-controller: `TestResetFailedAndWantedByAreOptionalVerbsOfTheServiceManager` (the verbs promised, + optional, read back optional from the store's row, each naming the command it replaces, and `why` + required of `unlink-dangling`), and the seat's verb list in `TestTheServiceManagerSeatServesTheUnitVerbs`. +- mesh-catalog `modules/systemd`: the `wanted_test.go` tests (a dangling enable link in the account's own + configuration found and named, a link matched by its target, an unreadable directory and an unreachable + manager said rather than read as nothing, read-only), and the manifest test that the module serves exactly + the verbs it claims. `TestAGlobIsRefusedAsAUnitByEveryVerb` covers every verb taking a unit. The + `unlink_test.go` tests check what is removed and what is left, that the record is written before each + removal, and that a link outside the account's home goes through `sudo -n`. +- Step 3 is not checked by anything yet, as ADR 0246 says of every optional verb.