Give back only the groups the mesh added, and say when a new login is needed (hq ADR 0252, issue 247)
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/module-groups delivered: every member is delivered

A module puts the operator's account in a group by declaring the account with that group alone.
The node-engine now records each group it added, takes back only those when nothing declared still
asks for them, refuses a group the machine lacks before usermod runs, and states each such account
as its module's resource of kind account: relogin needed while the running session lacks the group.
This commit is contained in:
jochen
2026-10-08 12:04:08 +02:00
parent ef8378a990
commit ab4ca44f98
13 changed files with 1096 additions and 31 deletions
+46 -3
View File
@@ -28,6 +28,7 @@ import (
"text/tabwriter"
"time"
"github.com/novox/mesh-host/internal/accounts"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/bundle"
"github.com/novox/mesh-host/internal/declaration"
@@ -1093,6 +1094,8 @@ func runLink(ctx context.Context, opts options) error {
judging = j
// And which units its service managers say failed, and whose each is (novox/hq issue 315).
unitJudge = units.New(units.Exec{Run: apply.ExecRunner, System: builtFor})
// And whether an account a module put in a group has it where it runs (novox/hq ADR 0252).
accountJudge = accounts.New(accounts.Exec{Run: accounts.Runner(apply.ExecRunner)})
}
// **Standing aside for a successor happens between reconciles and nowhere else** (novox/hq ADR
@@ -1391,6 +1394,10 @@ var judging *liveness.Judge
// issue 315); nil where judging is.
var unitJudge *units.Judge
// accountJudge reads whether an account a module put in a group has it in its running session (novox/hq
// ADR 0252); nil where judging is.
var accountJudge *accounts.Judge
// netJudge is the serving host's judge of its machine's networking (novox/hq ADR 0241); empty in a
// one-shot command and in a test, which say nothing of the network.
var netJudge networkJudge
@@ -1496,6 +1503,19 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Queue, sa
}
}
}
var accountSt *accounts.Statement
if a := accountJudge; a != nil {
as, accountsChanged := a.Look(ctx)
accountSt = &as
owed = owed || accountsChanged
if accountsChanged {
for _, v := range as.Accounts {
if v.State != accounts.Healthy {
say(fmt.Sprintf("%s (account %s) is %s: %s", v.ID, v.Name, v.State, v.Reason))
}
}
}
}
var netSt *network.Statement
if n := netJudge.get(); n != nil {
ns, netChanged := n.Look(ctx)
@@ -1519,7 +1539,7 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Queue, sa
}
since := time.Since(lastSaid)
healthy := st.Healthy() && (netSt == nil || netSt.State != network.Unhealthy) &&
(unitSt == nil || len(unitSt.Failed) == 0)
(unitSt == nil || len(unitSt.Failed) == 0) && (accountSt == nil || accountSt.Healthy())
if !owed && !(!healthy && since >= sayUnhealthyAgain) && since < sayAnyway {
continue
}
@@ -1531,7 +1551,7 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Queue, sa
}
}
}
if queue.SayHealth(ctx, *withUnits(healthAsReported(st, netSt), unitSt)) {
if queue.SayHealth(ctx, *withAccounts(withUnits(healthAsReported(st, netSt), unitSt), accountSt)) {
lastSaid, owed = time.Now(), false
}
}
@@ -1585,6 +1605,21 @@ func withUnits(h *link.Health, us *units.Statement) *link.Health {
return h
}
// withAccounts adds to a statement every account a module put in a group (novox/hq ADR 0252), as that
// module's resource of kind account: healthy, or unhealthy with why — "relogin needed" when the account's
// running session began before it was put in the group. Nil says nothing of them.
func withAccounts(h *link.Health, as *accounts.Statement) *link.Health {
if as == nil {
return h
}
for _, v := range as.Accounts {
h.Resources = append(h.Resources, link.ResourceHealth{Module: v.Module, Resource: v.ID,
Kind: link.KindAccount, Target: v.Name, State: v.State, Reason: v.Reason, Since: v.Since.UTC(),
Streak: v.Streak})
}
return h
}
// failedUnitWords is a failed unit as the console says it.
func failedUnitWords(f units.Failed) string {
whose := "no module places it"
@@ -1844,7 +1879,15 @@ func applyAndKeepHeld(ctx context.Context, opts options, raw []byte, signed *sto
us := u.Last()
unitSt = &us
}
report.Health = withUnits(healthAsReported(st, netSt), unitSt)
// Looked at now, not taken from the last look: an apply that just put the account in a group is
// said with it, relogin needed included, in the report that says the apply.
var accountSt *accounts.Statement
if a := accountJudge; a != nil {
a.Set(accounts.Of(declared, held))
as, _ := a.Look(ctx)
accountSt = &as
}
report.Health = withAccounts(withUnits(healthAsReported(st, netSt), unitSt), accountSt)
}
// Which of this machine's links face outside, for the filter the mesh writes around them
// (novox/hq ADR 0140). Reported whatever the node's mode: a converged node's filter needs it,