diff --git a/cmd/mesh-controller/probe_agent_root.go b/cmd/mesh-controller/probe_agent_root.go index 1c3ccccf..67e9b5be 100644 --- a/cmd/mesh-controller/probe_agent_root.go +++ b/cmd/mesh-controller/probe_agent_root.go @@ -32,8 +32,10 @@ import ( // or the bus's discovery hearing the verb answered there — so a withheld setting the machine has not acted // on yet, or a holder answering against its setting, is never taken for closed. // -// The measurement is the sudo module's on that machine (`sudo_escalation`); a machine it does not run on, or -// that does not answer, is a probe that could not run — said, never taken for "no". +// The measurement is the sudo module's on that machine (`sudo_escalation`). **What cannot be measured is not a +// pass**: a machine it does not run on, or that does not answer, raises the same urgent condition, saying it +// was not measured — the router reads the condition, and a probe that merely failed to run would leave it +// approving there (hq ADR 0259 §8, as amended on 2026-10-09). // kindAgentRoot is the condition an agent able to become root where a trusted party runs raises. const kindAgentRoot = "agent-root" @@ -183,10 +185,8 @@ func probeAgentRoot(ctx context.Context, d *doctor) ([]conditions.Observation, e // Whether the login shell's execute is served where a trusted party runs (novox/hq ADR 0268): the // holder's setting for that machine, and what the bus hears answered there. A discovery that could not be // asked leaves only the setting, which is said: the probe then cannot show the verb withheld. + // A discovery that could not be asked counts execute as served (loginShellServed), and says so. heard, derr := discoverSeatVerbs(ctx, d.js.Conn()) - if derr != nil { - unread = append(unread, "the bus's discovery could not be asked whether the login shell's execute is served: "+derr.Error()) - } for _, e := range entries { if !e.Manifest.ClaimsSeat(loginShellSeat) { continue @@ -200,8 +200,7 @@ func probeAgentRoot(ctx context.Context, d *doctor) ([]conditions.Observation, e if _, declared := e.Manifest.Settings[loginShellVerb]; declared { layers, err := inv.SettingsFor(ctx, node, e.Manifest.Module) if err != nil { - unread = append(unread, node+": "+e.Manifest.Module+"'s settings could not be read: "+err.Error()) - f.LoginShell, f.LoginShellWhy = true, "its holder's setting could not be read" + f.LoginShell, f.LoginShellWhy = true, e.Manifest.Module+"'s setting there could not be read: "+err.Error() continue } for _, s := range catalogue.Effective(e.Manifest, layers) { @@ -265,8 +264,35 @@ func probeAgentRoot(ctx context.Context, d *doctor) ([]conditions.Observation, e } out = append(out, agentRootObservations(*f)...) } - if len(unread) > 0 { - return out, fmt.Errorf("who can become root could not be measured: %s", strings.Join(unread, "; ")) + // Each machine whose measure failed is said as not measured, the same condition: never a pass. + for _, m := range machines { + var why []string + for _, u := range unread { + if strings.HasPrefix(u, m+": ") { + why = append(why, strings.TrimPrefix(u, m+": ")) + } + } + if len(why) > 0 { + out = append(out, agentRootUnmeasured(*facts[m], strings.Join(why, "; "))) + } } return out, nil } + +// agentRootUnmeasured is the condition of a machine where a trusted party runs and who can become root was +// not measured: urgent, like a measured yes, because the router believes a proof only where it is a no. +func agentRootUnmeasured(f agentRootFacts, why string) conditions.Observation { + trusted := append([]string(nil), f.Trusted...) + sort.Strings(trusted) + return conditions.Observation{Scope: conditions.ScopeMachine, ID: f.Machine, Token: kindAgentRoot, + Machine: f.Machine, Kind: kindAgentRoot, Severity: conditions.Urgent, + Summary: fmt.Sprintf("whether an agent can become root on %s without a person is not measured, where %s "+ + "run as accounts of their own; until it is, the router approves nothing proven there (novox/hq ADR "+ + "0259 §8): %s", f.Machine, strings.Join(trusted, ", "), why), + Headline: "Root not checked on " + f.Machine, + Needs: "let the mesh check who can become root on " + f.Machine + ": assign the sudo module there, or bring it back.", + Explanation: "The modules that prove your answers from your phone run on " + f.Machine + ", and the mesh " + + "could not check whether a program working for you there can become root without asking you. Until " + + "it can, answers from your phone can only acknowledge.", + Resolved: "The mesh checks who can become root on " + f.Machine + " again"} +} diff --git a/cmd/mesh-controller/probe_agent_root_test.go b/cmd/mesh-controller/probe_agent_root_test.go index 247e69f3..608c02bf 100644 --- a/cmd/mesh-controller/probe_agent_root_test.go +++ b/cmd/mesh-controller/probe_agent_root_test.go @@ -100,3 +100,24 @@ func TestTheLoginShellCountsOnlyWhereExecuteIsServed(t *testing.T) { t.Errorf("execute still answered: %+v", got) } } + +// hq ADR 0259 §8 as amended on 2026-10-09: a machine where a trusted party runs and who can become root is not +// measured raises the same urgent condition, so the router, which reads the condition, approves nothing there. +func TestRootNotMeasuredWhereTheRouterRunsIsSaidAndNeverAPass(t *testing.T) { + o := agentRootUnmeasured(agentRootFacts{Machine: "anchor", Trusted: []string{"telegram", "messenger"}}, + "the sudo module is not assigned there, so who can become root is not measured") + if o.Kind != kindAgentRoot || o.Severity != conditions.Urgent || o.Machine != "anchor" || + !strings.Contains(o.Summary, "not measured") || !strings.Contains(o.Summary, "messenger, telegram") { + t.Errorf("%+v", o) + } + if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation, + Needs: o.Needs, Resolved: o.Resolved}, "anchor"); !ok { + t.Errorf("not plain: %s", why) + } + // The same key as a measured yes, so the router's one rule reads both. + measured := agentRootObservations(agentRootFacts{Machine: "anchor", Trusted: []string{"telegram"}, Runtime: "ops", + Agent: "ops", Answers: map[string]escalation{"ops": {Root: true, Why: "x"}}})[0] + if o.Key() != measured.Key() { + t.Errorf("keys differ: %s, %s", o.Key(), measured.Key()) + } +}