Judge a machine root-free only on a positive, fresh measure, and believe a verified sender only there (hq ADR 0259 §8, review H2/H3)
The agent-root probe read the sudo module's answer, given in the machine's runtime as the very account an agent could become, and took a missing account, a missing answer or no accounts as a pass. One judgement now decides: the machine names an agent account its node-engine judged unable to become root within 15 minutes (agentConfined, mesh-controller #164), and the login shell's execute is not served there; anything not read is not free. The probe raises agent-root on it, the new root-free verb answers it live for the router, and a push composes verified-sender for a kind only while its machine and the router's pass it.
This commit is contained in:
@@ -2,51 +2,54 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// The self-check's probe of who can become root where the trusted parties run (novox/hq ADR 0259 §8, rule 3).
|
||||
// Who can become root where the trusted parties run (novox/hq ADR 0259 §8, as reviewed on 2026-10-09).
|
||||
//
|
||||
// The router and every channel proving its sender run as accounts of their own, so that no agent reads what
|
||||
// they hold or speaks as them. **Root on their machine undoes all of it.** So on every machine where a module
|
||||
// of its own account runs, this probe asks two questions, and raises an urgent condition while either is yes:
|
||||
// they hold or speaks as them. **Root on their machine undoes all of it**, and so does an agent running as the
|
||||
// operator's account there. A machine is **root-free** — an answer proven there may authorise — only when all
|
||||
// of these are measured, now, and hold:
|
||||
//
|
||||
// 1. can an account an agent runs as become root without a person there — passwordless sudo, or a group
|
||||
// that is root by another name (docker, disk)? The agent's account is the one the coding-agent module on
|
||||
// that machine names (`agent_account`), and the operator's account while it names none;
|
||||
// 2. can an agent run a command it chooses, through the mesh's own tools, as an account that can — the login
|
||||
// shell's `execute` runs as the machine's runtime account, which the mesh's acting tools give passwordless
|
||||
// sudo? **Only where `execute` is served** (novox/hq ADR 0268): `execute` is an optional verb its holder
|
||||
// withholds on a machine whose `execute` setting is not `serve`. The holder's seat being held there says
|
||||
// nothing; whether the verb is served does. It counts as served while either says so — the holder's
|
||||
// setting as the controller resolves it for that machine (a withheld value not yet pushed is still served),
|
||||
// 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.
|
||||
// 1. the machine names an account agents run as (novox/hq ADR 0266), so no agent runs as the operator's
|
||||
// account, which may become root;
|
||||
// 2. its node-engine — running as root, which no agent controls — judged that account unable to become root
|
||||
// without a person, in a statement heard within the last 15 minutes (agentConfined, judgedConfined);
|
||||
// 3. the login shell's `execute` is not served there (novox/hq ADR 0268): its holder's setting withholds it
|
||||
// **and** the bus was asked and heard no `execute` answered there. `execute` runs commands as the machine's
|
||||
// runtime account, which the mesh's acting tools give passwordless sudo; that account is taken to become
|
||||
// root, always, so no measure of it is asked.
|
||||
//
|
||||
// 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).
|
||||
// **Nothing else is a pass.** A machine that names no agent account, an unknown machine, a store or bus that
|
||||
// could not be read, a verdict stale or absent — each is not root-free, and says why. The sudo module's own
|
||||
// measure is no longer part of this judgement: it ran in the machine's runtime, as the very account an agent
|
||||
// could become, so it could not be believed.
|
||||
//
|
||||
// The one judgement (judgeRoot) is read two ways: the self-check raises `agent-root` on every machine where
|
||||
// the router or a module of its own account runs and the judgement fails; and the `root-free` verb answers it
|
||||
// live, to the router, which honours a verified sender only on its pass. A machine holding the operator's
|
||||
// graphical session, where a messaging client's desktop app may run, is not judged here: the operator accepted
|
||||
// that gap for now (hq issue 344).
|
||||
|
||||
// kindAgentRoot is the condition an agent able to become root where a trusted party runs raises.
|
||||
// kindAgentRoot is the condition a trusted party's machine that is not root-free raises.
|
||||
const kindAgentRoot = "agent-root"
|
||||
|
||||
// The tools the probe asks, of the modules on each machine.
|
||||
// routerSeat is the seat the router holds: where it runs counts as a trusted party's machine.
|
||||
const routerSeat = "operator-channel"
|
||||
|
||||
const (
|
||||
agentModule = "claude-code"
|
||||
agentStatusTool = "claude_code_status"
|
||||
sudoModule = "sudo"
|
||||
sudoEscalation = "sudo_escalation"
|
||||
loginShellSeat = "node-login-shell"
|
||||
loginShellSeat = "node-login-shell"
|
||||
// loginShellVerb is the seat's verb that runs a command, and the name of the setting its holder withholds
|
||||
// it by (novox/hq ADR 0268).
|
||||
loginShellVerb = "execute"
|
||||
@@ -57,7 +60,7 @@ const (
|
||||
// loginShellServed judges whether the login shell's execute is served on a machine, failing closed (novox/hq
|
||||
// ADR 0268): served while the holder's setting there is `serve` (or the holder has no such setting and claims
|
||||
// the verb), or while the bus heard the verb answered there, or while the bus could not be asked. why says
|
||||
// which, in words, for the condition.
|
||||
// which, in words.
|
||||
func loginShellServed(holder string, claims bool, setting *any, heard, asked bool) (bool, string) {
|
||||
var why []string
|
||||
switch {
|
||||
@@ -76,6 +79,152 @@ func loginShellServed(holder string, claims bool, setting *any, heard, asked boo
|
||||
return len(why) > 0, strings.Join(why, "; ")
|
||||
}
|
||||
|
||||
// rootFacts is what the judgement reads of one machine.
|
||||
type rootFacts struct {
|
||||
Machine string
|
||||
// Unread is every read that failed, in words: any one is a fail.
|
||||
Unread []string
|
||||
// AgentNamed is whether the machine names an agent account; Confined whether its node-engine judged it
|
||||
// unable to become root, freshly; ConfinedWhy the judgement's words either way.
|
||||
AgentNamed bool
|
||||
Confined bool
|
||||
ConfinedWhy string
|
||||
// Execute is whether the login shell's execute is served there; ExecuteWhy why, in words.
|
||||
Execute bool
|
||||
ExecuteWhy string
|
||||
}
|
||||
|
||||
// rootVerdict is the judgement on one machine, as the root-free verb answers it.
|
||||
type rootVerdict struct {
|
||||
Machine string `json:"machine"`
|
||||
Free bool `json:"free"`
|
||||
Why string `json:"why"`
|
||||
Judged time.Time `json:"judged"`
|
||||
}
|
||||
|
||||
// judgeRoot is the one judgement: free only when nothing failed to read, an agent account is named and judged
|
||||
// confined, and execute is not served.
|
||||
func judgeRoot(f rootFacts, now time.Time) rootVerdict {
|
||||
v := rootVerdict{Machine: f.Machine, Judged: now.UTC()}
|
||||
var not []string
|
||||
if len(f.Unread) > 0 {
|
||||
not = append(not, "not measured: "+strings.Join(f.Unread, "; "))
|
||||
}
|
||||
switch {
|
||||
case f.ConfinedWhy == "":
|
||||
// Not read (said above), or nothing said of it: never a pass.
|
||||
if len(f.Unread) == 0 {
|
||||
not = append(not, "whether agents there can become root was not judged")
|
||||
}
|
||||
case !f.AgentNamed:
|
||||
not = append(not, "agents run as the operator's account there, which may become root ("+f.ConfinedWhy+")")
|
||||
case !f.Confined:
|
||||
not = append(not, f.ConfinedWhy)
|
||||
}
|
||||
if f.Execute {
|
||||
not = append(not, "the login shell runs any command an agent gives it as the machine's runtime account, "+
|
||||
"which can become root ("+orNoneKnown(f.ExecuteWhy)+")")
|
||||
}
|
||||
if len(not) > 0 {
|
||||
v.Why = strings.Join(not, "; ")
|
||||
return v
|
||||
}
|
||||
v.Free = true
|
||||
v.Why = f.ConfinedWhy + "; the login shell's execute is not served there"
|
||||
return v
|
||||
}
|
||||
|
||||
// rootReader reads the facts of machines live: the catalogue's placements, the node-engine's verdicts and the
|
||||
// bus's discovery, each once per reader.
|
||||
type rootReader struct {
|
||||
entries []inventory.Entry
|
||||
read error
|
||||
heard map[string]map[string]map[string]bool
|
||||
asked error
|
||||
// confined is agentConfined; settings the login shell holder's settings on a machine. Replaceable in a test.
|
||||
confined func(ctx context.Context, node string) (named, confined bool, why string, err error)
|
||||
settings func(ctx context.Context, node, module string) ([]catalogue.Layer, error)
|
||||
}
|
||||
|
||||
func newRootReader(ctx context.Context, inv *inventory.Inventory, conn *nats.Conn) *rootReader {
|
||||
r := &rootReader{}
|
||||
r.entries, r.read = inv.Catalogued(ctx)
|
||||
if conn == nil {
|
||||
r.asked = fmt.Errorf("this process holds no connection to the bus")
|
||||
} else {
|
||||
r.heard, r.asked = discoverSeatVerbs(ctx, conn)
|
||||
}
|
||||
r.confined = func(ctx context.Context, node string) (bool, bool, string, error) {
|
||||
return agentConfined(ctx, inv, node)
|
||||
}
|
||||
r.settings = inv.SettingsFor
|
||||
return r
|
||||
}
|
||||
|
||||
// facts reads one machine.
|
||||
func (r *rootReader) facts(ctx context.Context, machine string) rootFacts {
|
||||
f := rootFacts{Machine: machine}
|
||||
if r.read != nil {
|
||||
f.Unread = append(f.Unread, "the catalogue's placements could not be read: "+r.read.Error())
|
||||
}
|
||||
named, confined, why, err := r.confined(ctx, machine)
|
||||
if err != nil {
|
||||
f.Unread = append(f.Unread, "the account agents run as could not be read: "+err.Error())
|
||||
} else {
|
||||
f.AgentNamed, f.Confined, f.ConfinedWhy = named, confined, why
|
||||
}
|
||||
f.Execute, f.ExecuteWhy = r.executeServed(ctx, machine)
|
||||
return f
|
||||
}
|
||||
|
||||
// executeServed is whether the login shell's execute is served on a machine, failing closed: the bus not
|
||||
// asked, the placements not read, or a holder's setting not read, is served.
|
||||
func (r *rootReader) executeServed(ctx context.Context, machine string) (bool, string) {
|
||||
heard := r.heard[loginShellSeat][loginShellVerb][machine]
|
||||
asked := r.asked == nil
|
||||
var whys []string
|
||||
served := false
|
||||
holders := 0
|
||||
for _, e := range r.entries {
|
||||
if !e.Manifest.ClaimsSeat(loginShellSeat) || !slices.Contains(e.On, machine) {
|
||||
continue
|
||||
}
|
||||
holders++
|
||||
var setting *any
|
||||
if _, declared := e.Manifest.Settings[loginShellVerb]; declared {
|
||||
layers, err := r.settings(ctx, machine, e.Manifest.Module)
|
||||
if err != nil {
|
||||
served = true
|
||||
whys = append(whys, e.Manifest.Module+"'s setting there could not be read: "+err.Error())
|
||||
continue
|
||||
}
|
||||
for _, s := range catalogue.Effective(e.Manifest, layers) {
|
||||
if s.Key == loginShellVerb {
|
||||
v := s.Value
|
||||
setting = &v
|
||||
}
|
||||
}
|
||||
}
|
||||
if s, why := loginShellServed(e.Manifest.Module, claimServes(e.Manifest, loginShellSeat, loginShellVerb),
|
||||
setting, heard, asked); s {
|
||||
served = true
|
||||
whys = append(whys, why)
|
||||
}
|
||||
}
|
||||
if holders == 0 {
|
||||
// Nobody is assigned to serve it; the bus must still hear nobody answering it.
|
||||
if s, why := loginShellServed("no holder", false, nil, heard, asked); s {
|
||||
served = true
|
||||
whys = append(whys, why)
|
||||
}
|
||||
}
|
||||
if r.read != nil {
|
||||
served = true
|
||||
whys = append(whys, "who holds the login shell there could not be read")
|
||||
}
|
||||
return served, strings.Join(whys, "; ")
|
||||
}
|
||||
|
||||
// claimServes says whether a manifest's claim of a seat names a verb among those it serves.
|
||||
func claimServes(m catalogue.Manifest, seat, verb string) bool {
|
||||
for _, c := range m.Claims {
|
||||
@@ -86,213 +235,109 @@ func claimServes(m catalogue.Manifest, seat, verb string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// escalation is the sudo module's answer for one account.
|
||||
type escalation struct {
|
||||
Account string `json:"account"`
|
||||
Root bool `json:"root_without_a_person"`
|
||||
Why string `json:"why"`
|
||||
// judgeRootFree is the root-free verb's answer: each named machine judged now. It never fails: what could not
|
||||
// be read is a machine not free, saying so.
|
||||
func judgeRootFree(ctx context.Context, r *rootReader, machines []string, now time.Time) []rootVerdict {
|
||||
out := make([]rootVerdict, 0, len(machines))
|
||||
for _, m := range machines {
|
||||
out = append(out, judgeRoot(r.facts(ctx, m), now))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// agentRootFacts is what one machine says, as the probe reads it.
|
||||
type agentRootFacts struct {
|
||||
Machine string
|
||||
Trusted []string // the modules of their own account on it
|
||||
Agent string // the account agents run as there; "" when none run there
|
||||
AgentNamed bool // the coding-agent module named it, rather than it being taken for the operator's
|
||||
Runtime string // the account the machine's runtime runs as
|
||||
LoginShell bool // the login shell's execute is served there, running commands as the runtime's account
|
||||
// LoginShellWhy is why execute counts as served there, in words: its holder's setting, the bus, or both.
|
||||
LoginShellWhy string
|
||||
Answers map[string]escalation
|
||||
}
|
||||
|
||||
// agentRootObservations judges one machine's facts: an urgent condition while an agent can become root there
|
||||
// without a person, one way or the other, naming which.
|
||||
func agentRootObservations(f agentRootFacts) []conditions.Observation {
|
||||
var ways []string
|
||||
if f.Agent != "" {
|
||||
if e := f.Answers[f.Agent]; e.Root {
|
||||
named := "the operator's account, which agents run as while the coding-agent module names no other"
|
||||
if f.AgentNamed {
|
||||
named = "the account agents run as"
|
||||
}
|
||||
ways = append(ways, fmt.Sprintf("%s (%s) can become root without a person: %s", f.Agent, named, e.Why))
|
||||
}
|
||||
}
|
||||
if f.LoginShell && f.Runtime != "" {
|
||||
if e := f.Answers[f.Runtime]; e.Root {
|
||||
served := ""
|
||||
if f.LoginShellWhy != "" {
|
||||
served = " (" + f.LoginShellWhy + ")"
|
||||
}
|
||||
ways = append(ways, fmt.Sprintf("the login shell runs any command an agent gives it as %s%s, which can "+
|
||||
"become root without a person: %s", f.Runtime, served, e.Why))
|
||||
}
|
||||
}
|
||||
if len(ways) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(f.Trusted)
|
||||
return []conditions.Observation{{Scope: conditions.ScopeMachine, ID: f.Machine, Token: kindAgentRoot,
|
||||
Machine: f.Machine, Kind: kindAgentRoot, Severity: conditions.Urgent,
|
||||
Summary: fmt.Sprintf("an agent can become root on %s without a person, where %s run as accounts of their "+
|
||||
"own; until it cannot, the router approves nothing proven there (novox/hq ADR 0259 §8): %s",
|
||||
f.Machine, strings.Join(f.Trusted, ", "), strings.Join(ways, "; ")),
|
||||
Headline: "Root without you on " + f.Machine,
|
||||
Needs: "choose how programs working for you on " + f.Machine + " stop becoming root without asking you.",
|
||||
Explanation: "The modules that prove your answers from your phone run on " + f.Machine + ", and a program " +
|
||||
"working for you there can become root without asking you, so it could answer in your name. Until " +
|
||||
"that changes, answers from your phone can only acknowledge.",
|
||||
Resolved: "Nothing on " + f.Machine + " becomes root without you any more"}}
|
||||
}
|
||||
|
||||
// probeAgentRoot is the probe: every machine a module of its own account runs on, judged.
|
||||
func probeAgentRoot(ctx context.Context, d *doctor) ([]conditions.Observation, error) {
|
||||
inv := d.open.inventory
|
||||
entries, err := inv.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
facts := map[string]*agentRootFacts{}
|
||||
agentOn, sudoOn := map[string]bool{}, map[string]bool{}
|
||||
// trustedMachines are the machines where the router or a module of its own account runs, each with those
|
||||
// modules.
|
||||
func trustedMachines(entries []inventory.Entry) map[string][]string {
|
||||
trusted := map[string][]string{}
|
||||
for _, e := range entries {
|
||||
for _, node := range e.On {
|
||||
switch {
|
||||
case e.Manifest.RunsAs != "":
|
||||
f := facts[node]
|
||||
if f == nil {
|
||||
f = &agentRootFacts{Machine: node, Answers: map[string]escalation{}}
|
||||
facts[node] = f
|
||||
}
|
||||
f.Trusted = append(f.Trusted, e.Manifest.Module)
|
||||
case e.Manifest.Module == agentModule:
|
||||
agentOn[node] = true
|
||||
case e.Manifest.Module == sudoModule:
|
||||
sudoOn[node] = true
|
||||
if e.Manifest.RunsAs != "" || e.Manifest.ClaimsSeat(routerSeat) {
|
||||
trusted[node] = append(trusted[node], e.Manifest.Module)
|
||||
}
|
||||
}
|
||||
}
|
||||
machines := make([]string, 0, len(facts))
|
||||
for m := range facts {
|
||||
return trusted
|
||||
}
|
||||
|
||||
// agentRootObservation is the condition of a trusted party's machine that is not root-free.
|
||||
func agentRootObservation(v rootVerdict, trusted []string) conditions.Observation {
|
||||
trusted = append([]string(nil), trusted...)
|
||||
sort.Strings(trusted)
|
||||
return conditions.Observation{Scope: conditions.ScopeMachine, ID: v.Machine, Token: kindAgentRoot,
|
||||
Machine: v.Machine, Kind: kindAgentRoot, Severity: conditions.Urgent,
|
||||
Summary: fmt.Sprintf("%s is not root-free, where %s run: until it is, the router approves nothing proven "+
|
||||
"there (novox/hq ADR 0259 §8): %s", v.Machine, strings.Join(trusted, ", "), v.Why),
|
||||
Headline: "Phone answers held on " + v.Machine,
|
||||
Needs: "give the programs working for you on " + v.Machine + " an account that cannot become root.",
|
||||
Explanation: "The modules that prove your answers from your phone run on " + v.Machine + ", and the mesh " +
|
||||
"cannot show that a program working for you there is unable to become root or to act as you. Until " +
|
||||
"it can, answers from your phone can only acknowledge.",
|
||||
Resolved: "Answers from your phone can approve again on " + v.Machine}
|
||||
}
|
||||
|
||||
// probeAgentRoot is the probe: every trusted party's machine, judged by the one judgement.
|
||||
func probeAgentRoot(ctx context.Context, d *doctor) ([]conditions.Observation, error) {
|
||||
var conn *nats.Conn
|
||||
if d.js != nil {
|
||||
conn = d.js.Conn()
|
||||
}
|
||||
r := newRootReader(ctx, d.open.inventory, conn)
|
||||
if r.read != nil {
|
||||
return nil, r.read
|
||||
}
|
||||
return rootObservations(ctx, r, trustedMachines(r.entries), time.Now()), nil
|
||||
}
|
||||
|
||||
// rootObservations judges the machines and says each that fails.
|
||||
func rootObservations(ctx context.Context, r *rootReader, trusted map[string][]string, now time.Time) []conditions.Observation {
|
||||
var machines []string
|
||||
for m := range trusted {
|
||||
machines = append(machines, m)
|
||||
}
|
||||
sort.Strings(machines)
|
||||
var out []conditions.Observation
|
||||
var unread []string
|
||||
if len(machines) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// 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())
|
||||
for _, e := range entries {
|
||||
if !e.Manifest.ClaimsSeat(loginShellSeat) {
|
||||
continue
|
||||
}
|
||||
for _, node := range e.On {
|
||||
f := facts[node]
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
var setting *any
|
||||
if _, declared := e.Manifest.Settings[loginShellVerb]; declared {
|
||||
layers, err := inv.SettingsFor(ctx, node, e.Manifest.Module)
|
||||
if err != nil {
|
||||
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) {
|
||||
if s.Key == loginShellVerb {
|
||||
v := s.Value
|
||||
setting = &v
|
||||
}
|
||||
}
|
||||
}
|
||||
f.LoginShell, f.LoginShellWhy = loginShellServed(e.Manifest.Module, claimServes(e.Manifest, loginShellSeat, loginShellVerb),
|
||||
setting, heard[loginShellSeat][loginShellVerb][node], derr == nil)
|
||||
for _, v := range judgeRootFree(ctx, r, machines, now) {
|
||||
if !v.Free {
|
||||
out = append(out, agentRootObservation(v, trusted[v.Machine]))
|
||||
}
|
||||
}
|
||||
for _, m := range machines {
|
||||
f := facts[m]
|
||||
record, err := inv.NodeByName(ctx, m)
|
||||
if err != nil {
|
||||
unread = append(unread, m+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
f.Runtime = record.Account
|
||||
if agentOn[m] {
|
||||
f.Agent = record.Account
|
||||
if a, err := link.AskModuleToolOn(ctx, d.js.Conn(), agentModule, agentStatusTool, m, map[string]any{}, 10*time.Second); err == nil && a.Error == "" {
|
||||
var status struct {
|
||||
AgentAccount string `json:"agent_account"`
|
||||
}
|
||||
if json.Unmarshal(a.Result, &status) == nil && status.AgentAccount != "" {
|
||||
f.Agent, f.AgentNamed = status.AgentAccount, true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sudoOn[m] {
|
||||
unread = append(unread, m+": the sudo module is not assigned there, so who can become root is not measured")
|
||||
continue
|
||||
}
|
||||
var accounts []string
|
||||
for _, a := range []string{f.Agent, f.Runtime} {
|
||||
if a != "" && !slices.Contains(accounts, a) {
|
||||
accounts = append(accounts, a)
|
||||
}
|
||||
}
|
||||
if len(accounts) == 0 {
|
||||
continue
|
||||
}
|
||||
a, err := link.AskModuleToolOn(ctx, d.js.Conn(), sudoModule, sudoEscalation, m, map[string]any{"accounts": accounts}, 15*time.Second)
|
||||
if err == nil && a.Error != "" {
|
||||
err = fmt.Errorf("%s", a.Error)
|
||||
}
|
||||
if err != nil {
|
||||
unread = append(unread, m+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
var answers []escalation
|
||||
if err := json.Unmarshal(a.Result, &answers); err != nil {
|
||||
unread = append(unread, m+": the sudo module's answer could not be read: "+err.Error())
|
||||
continue
|
||||
}
|
||||
for _, e := range answers {
|
||||
f.Answers[e.Account] = e
|
||||
}
|
||||
out = append(out, agentRootObservations(*f)...)
|
||||
}
|
||||
// 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
|
||||
return out
|
||||
}
|
||||
|
||||
// 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"}
|
||||
// rootClock is the clock the root-free verb judges by.
|
||||
var rootClock = time.Now
|
||||
|
||||
// rootFreeAnswer is the root-free verb: the named machines, each judged now by the serving controller. Only it
|
||||
// answers: a process that is not serving says so, and a caller reads that as no machine free.
|
||||
func rootFreeAnswer(ctx context.Context, machines string, now time.Time) (any, error) {
|
||||
d := doctorFrom
|
||||
if d == nil || d.open == nil || d.open.inventory == nil {
|
||||
return nil, fmt.Errorf("this controller is not serving, so it judges no machine root-free: ask again, and " +
|
||||
"the serving controller answers")
|
||||
}
|
||||
var names []string
|
||||
for _, m := range strings.Split(machines, ",") {
|
||||
if m = strings.TrimSpace(m); m != "" && !slices.Contains(names, m) {
|
||||
names = append(names, m)
|
||||
}
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil, fmt.Errorf("root-free judges the machines named, and none was")
|
||||
}
|
||||
var conn *nats.Conn
|
||||
if d.js != nil {
|
||||
conn = d.js.Conn()
|
||||
}
|
||||
return map[string]any{"machines": judgeRootFree(ctx, newRootReader(ctx, d.open.inventory, conn), names, now)}, nil
|
||||
}
|
||||
|
||||
// rootFreeNow is the machines judged root-free, for composing a push's memberships: only those that pass.
|
||||
func rootFreeNow(ctx context.Context, r *rootReader, machines []string, now time.Time) map[string]bool {
|
||||
free := map[string]bool{}
|
||||
for _, v := range judgeRootFree(ctx, r, machines, now) {
|
||||
if v.Free {
|
||||
free[v.Machine] = true
|
||||
}
|
||||
}
|
||||
return free
|
||||
}
|
||||
|
||||
@@ -1,66 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// novox/hq ADR 0259 §8, rule 3: the probe fails today — agents run as the operator's account, which has
|
||||
// passwordless sudo, and the login shell runs their commands as it — and passes only once neither holds.
|
||||
func TestAnAgentAbleToBecomeRootWhereTheRouterRunsIsSaid(t *testing.T) {
|
||||
root := escalation{Root: true, Why: "(ALL : ALL) NOPASSWD: ALL"}
|
||||
none := escalation{Why: "no rule lets it without a password"}
|
||||
base := func() agentRootFacts {
|
||||
return agentRootFacts{Machine: "anchor", Trusted: []string{"telegram", "messenger"}, Runtime: "ops",
|
||||
Answers: map[string]escalation{}}
|
||||
}
|
||||
var rootNow = time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
today := base()
|
||||
today.Agent, today.LoginShell = "ops", true
|
||||
today.Answers["ops"] = root
|
||||
got := agentRootObservations(today)
|
||||
if len(got) != 1 || got[0].Severity != conditions.Urgent || got[0].Kind != kindAgentRoot ||
|
||||
!strings.Contains(got[0].Summary, "the operator's account") || !strings.Contains(got[0].Summary, "the login shell") {
|
||||
t.Fatalf("today: %+v", got)
|
||||
// A machine is root-free only on a positive measure of each thing (the review of 2026-10-09, H2/H3): every
|
||||
// read succeeded, an agent account is named and judged confined by the node-engine, execute is not served.
|
||||
func TestRootFreeIsAPositiveMeasureAndNothingElse(t *testing.T) {
|
||||
pass := rootFacts{Machine: "anchor", AgentNamed: true, Confined: true,
|
||||
ConfinedWhy: "the agent account agents cannot become root without a person (judged 2026-10-09 12:00)"}
|
||||
if v := judgeRoot(pass, rootNow); !v.Free || v.Machine != "anchor" || !v.Judged.Equal(rootNow) {
|
||||
t.Fatalf("the one pass: %+v", v)
|
||||
}
|
||||
|
||||
agentsMoved := base()
|
||||
agentsMoved.Agent, agentsMoved.AgentNamed, agentsMoved.LoginShell = "agents", true, true
|
||||
agentsMoved.Answers["agents"], agentsMoved.Answers["ops"] = none, root
|
||||
if got := agentRootObservations(agentsMoved); len(got) != 1 || strings.Contains(got[0].Summary, "agents (") ||
|
||||
!strings.Contains(got[0].Summary, "the login shell") {
|
||||
t.Errorf("agents of their own account, the login shell still the runtime's: %+v", got)
|
||||
fails := map[string]func(*rootFacts){
|
||||
"a read failed": func(f *rootFacts) { f.Unread = []string{"the store did not answer"} },
|
||||
"no agent account named": func(f *rootFacts) { f.AgentNamed, f.Confined = false, false },
|
||||
"not confined": func(f *rootFacts) { f.Confined = false },
|
||||
"nothing said of it": func(f *rootFacts) { f.ConfinedWhy = "" },
|
||||
"execute served": func(f *rootFacts) { f.Execute, f.ExecuteWhy = true, "the bus hears execute answered there" },
|
||||
"confined, but agent read": func(f *rootFacts) { f.Unread, f.ConfinedWhy = []string{"x"}, "" },
|
||||
}
|
||||
|
||||
closed := base()
|
||||
closed.Agent, closed.AgentNamed = "agents", true
|
||||
closed.Answers["agents"], closed.Answers["ops"] = none, root
|
||||
if got := agentRootObservations(closed); len(got) != 0 {
|
||||
t.Errorf("agents of their own account and no login shell there: %+v", got)
|
||||
}
|
||||
|
||||
noAgents := base()
|
||||
noAgents.Answers["ops"] = root
|
||||
if got := agentRootObservations(noAgents); len(got) != 0 {
|
||||
t.Errorf("no agent runs there and no login shell is held: %+v", got)
|
||||
for name, mutate := range fails {
|
||||
f := pass
|
||||
mutate(&f)
|
||||
if v := judgeRoot(f, rootNow); v.Free || v.Why == "" {
|
||||
t.Errorf("%s: judged %+v", name, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Its words are plain, as every condition's are (novox/hq ADR 0253).
|
||||
func TestTheRootConditionIsSaidInPlainWords(t *testing.T) {
|
||||
f := agentRootFacts{Machine: "anchor", Trusted: []string{"telegram"}, Runtime: "ops", Agent: "ops",
|
||||
Answers: map[string]escalation{"ops": {Root: true, Why: "x"}}}
|
||||
o := agentRootObservations(f)[0]
|
||||
// The reader fails closed on every case the review named: the agent account read only where the coding-agent
|
||||
// module runs (old :225), no account to measure taken for a pass (old :246), and a measure that did not answer
|
||||
// taken for "not root" (old :114, :123).
|
||||
func TestTheRootReaderFailsClosed(t *testing.T) {
|
||||
shell := catalogue.Manifest{Module: "zsh", Claims: []catalogue.Claim{{Name: loginShellSeat, Serves: []string{"execute"}}},
|
||||
Settings: map[string]catalogue.SettingDeclaration{"execute": {Default: "withhold"}}}
|
||||
reader := func() *rootReader {
|
||||
return &rootReader{
|
||||
entries: []inventory.Entry{{Manifest: shell, On: []string{"anchor"}}},
|
||||
heard: map[string]map[string]map[string]bool{},
|
||||
confined: func(context.Context, string) (bool, bool, string, error) {
|
||||
return true, true, "the agent account agents cannot become root without a person", nil
|
||||
},
|
||||
settings: func(context.Context, string, string) ([]catalogue.Layer, error) { return nil, nil },
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
if v := judgeRootFree(ctx, reader(), []string{"anchor"}, rootNow)[0]; !v.Free {
|
||||
t.Fatalf("the control: agents confined, execute withheld and unheard, everything read: %+v", v)
|
||||
}
|
||||
cases := map[string]func(*rootReader){
|
||||
"no agent account named, no coding-agent module there": func(r *rootReader) {
|
||||
r.confined = func(context.Context, string) (bool, bool, string, error) {
|
||||
return false, false, "anchor names no agent account: agents run as the operator account (ops)", nil
|
||||
}
|
||||
},
|
||||
"the node-engine's verdict not read": func(r *rootReader) {
|
||||
r.confined = func(context.Context, string) (bool, bool, string, error) {
|
||||
return false, false, "", errors.New("the store did not answer")
|
||||
}
|
||||
},
|
||||
"a stale verdict": func(r *rootReader) {
|
||||
r.confined = func(context.Context, string) (bool, bool, string, error) {
|
||||
return true, false, "the agent account agents is not judged: the machine's newest statement was heard at …", nil
|
||||
}
|
||||
},
|
||||
"the bus not asked": func(r *rootReader) { r.asked = errors.New("no bus") },
|
||||
"the placements not read": func(r *rootReader) { r.read = errors.New("no store") },
|
||||
"the holder's setting not read": func(r *rootReader) {
|
||||
r.settings = func(context.Context, string, string) ([]catalogue.Layer, error) { return nil, errors.New("no store") }
|
||||
},
|
||||
"execute heard on the bus": func(r *rootReader) {
|
||||
r.heard = map[string]map[string]map[string]bool{loginShellSeat: {"execute": {"anchor": true}}}
|
||||
},
|
||||
"a holder that serves execute": func(r *rootReader) {
|
||||
r.entries[0].Manifest.Settings = nil
|
||||
},
|
||||
}
|
||||
for name, mutate := range cases {
|
||||
r := reader()
|
||||
mutate(r)
|
||||
if v := judgeRootFree(ctx, r, []string{"anchor"}, rootNow)[0]; v.Free {
|
||||
t.Errorf("%s: judged free: %+v", name, v)
|
||||
}
|
||||
}
|
||||
// A machine with no login shell holder at all: still the bus must hear none.
|
||||
r := reader()
|
||||
r.entries = nil
|
||||
r.heard = map[string]map[string]map[string]bool{loginShellSeat: {"execute": {"anchor": true}}}
|
||||
if v := judgeRootFree(ctx, r, []string{"anchor"}, rootNow)[0]; v.Free {
|
||||
t.Errorf("execute answered by a module nobody assigned: %+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// The probe says agent-root, by the same judgement, on each machine where the router or a module of its own
|
||||
// account runs and that is not root-free; urgent and in plain words; nothing where every one is free.
|
||||
func TestTheProbeSaysEachMachineThatIsNotRootFree(t *testing.T) {
|
||||
entries := []inventory.Entry{
|
||||
{Manifest: catalogue.Manifest{Module: "telegram", RunsAs: "telegram"}, On: []string{"anchor"}},
|
||||
{Manifest: catalogue.Manifest{Module: "messenger", RunsAs: "messenger",
|
||||
Claims: []catalogue.Claim{{Name: routerSeat}}}, On: []string{"anchor"}},
|
||||
{Manifest: catalogue.Manifest{Module: "xorg", Claims: []catalogue.Claim{{Name: catalogue.DisplayServerSeat}}},
|
||||
On: []string{"laptop"}},
|
||||
}
|
||||
trusted := trustedMachines(entries)
|
||||
if len(trusted["anchor"]) != 2 || len(trusted["laptop"]) != 0 {
|
||||
t.Fatalf("trusted %v", trusted)
|
||||
}
|
||||
r := &rootReader{entries: entries, heard: map[string]map[string]map[string]bool{},
|
||||
confined: func(_ context.Context, node string) (bool, bool, string, error) {
|
||||
return false, false, node + " names no agent account: agents run as the operator account (ops)", nil
|
||||
},
|
||||
settings: func(context.Context, string, string) ([]catalogue.Layer, error) { return nil, nil }}
|
||||
got := rootObservations(context.Background(), r, trusted, rootNow)
|
||||
if len(got) != 1 || got[0].Machine != "anchor" || got[0].Kind != kindAgentRoot || got[0].Severity != conditions.Urgent ||
|
||||
!strings.Contains(got[0].Summary, "messenger, telegram") || !strings.Contains(got[0].Summary, "names no agent account") {
|
||||
t.Fatalf("said %+v", got)
|
||||
}
|
||||
o := got[0]
|
||||
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
|
||||
Needs: o.Needs, Resolved: o.Resolved}, "anchor"); !ok {
|
||||
Needs: o.Needs, Resolved: o.Resolved}, o.Machine); !ok {
|
||||
t.Errorf("not plain: %s", why)
|
||||
}
|
||||
r.confined = func(context.Context, string) (bool, bool, string, error) { return true, true, "confined", nil }
|
||||
if got := rootObservations(context.Background(), r, trusted, rootNow); len(got) != 0 {
|
||||
t.Errorf("said of a free machine: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0268: the login shell counts only where its execute is served, and fails closed — the holder's
|
||||
// setting resolving to anything but serve and the bus hearing no execute there is the one way it is withheld.
|
||||
// novox/hq ADR 0268: the login shell counts only where its execute is served, and fails closed.
|
||||
func TestTheLoginShellCountsOnlyWhereExecuteIsServed(t *testing.T) {
|
||||
val := func(v any) *any { return &v }
|
||||
cases := []struct {
|
||||
@@ -86,38 +164,24 @@ func TestTheLoginShellCountsOnlyWhereExecuteIsServed(t *testing.T) {
|
||||
t.Errorf("%s: served %v (%q), want %v saying %q", c.name, served, why, c.served, c.saysInTheWhys)
|
||||
}
|
||||
}
|
||||
|
||||
// The control-node with execute withheld and agents of their own account: nothing is said.
|
||||
f := agentRootFacts{Machine: "anchor", Trusted: []string{"telegram"}, Runtime: "ops", Agent: "agents", AgentNamed: true,
|
||||
Answers: map[string]escalation{"ops": {Root: true, Why: "NOPASSWD"}, "agents": {Why: "none"}}}
|
||||
f.LoginShell, f.LoginShellWhy = loginShellServed("zsh", true, val("withhold"), false, true)
|
||||
if got := agentRootObservations(f); len(got) != 0 {
|
||||
t.Errorf("execute withheld and agents confined: %+v", got)
|
||||
}
|
||||
// Withheld in the setting, still answered on the bus: said, with why.
|
||||
f.LoginShell, f.LoginShellWhy = loginShellServed("zsh", true, val("withhold"), true, true)
|
||||
if got := agentRootObservations(f); len(got) != 1 || !strings.Contains(got[0].Summary, "the bus hears execute answered there") {
|
||||
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)
|
||||
// The root-free verb is the serving controller's alone, answered in its process and never as a command; a
|
||||
// controller not serving answers an error, which the router reads as no machine free.
|
||||
func TestRootFreeIsAnsweredOnlyByTheServingController(t *testing.T) {
|
||||
was := doctorFrom
|
||||
doctorFrom = nil
|
||||
t.Cleanup(func() { doctorFrom = was })
|
||||
if _, err := rootFreeAnswer(context.Background(), "anchor", rootNow); err == nil {
|
||||
t.Error("a controller not serving judged a machine")
|
||||
}
|
||||
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)
|
||||
if !inProcess["root-free"] {
|
||||
t.Error("root-free is not answered in the serving process")
|
||||
}
|
||||
// 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())
|
||||
if _, err := argvFor("root-free", map[string]any{"machines": "anchor"}); err == nil {
|
||||
t.Error("root-free ran as a command")
|
||||
}
|
||||
if _, err := readArguments("root-free", map[string]any{"machines": []any{"anchor"}}); err == nil {
|
||||
t.Error("root-free took its machines as a list; they are named in one text, separated by commas")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1242,11 +1242,14 @@ func issueMemberships(ctx context.Context, open *stores, server *link.Server, se
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
where := broker.PlacementsOf(records, records.Interchangeable)
|
||||
bus, ok := server.Bus().(link.OverNATS)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Which machines are root-free now (novox/hq ADR 0259 §8): a channel's verified sender is composed for the
|
||||
// router only from one, beside a router on one. Judged once per push, by the root-free verb's judgement.
|
||||
records.RootFree = rootFreeNow(ctx, newRootReader(ctx, open.inventory, bus.Conn), records.Nodes, time.Now())
|
||||
where := broker.PlacementsOf(records, records.Interchangeable)
|
||||
// **Every declared state's bucket, before the memberships that name it** (novox/hq ADR 0201). The
|
||||
// raise at start asserts them too, but a module registered and assigned since would otherwise have
|
||||
// its bucket only after the control plane next restarts — found the first time a module declared
|
||||
|
||||
@@ -272,6 +272,8 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
||||
return nil, errors.New("tools is answered from the records, not by a command")
|
||||
case "dead-letters":
|
||||
return nil, errors.New("dead-letters is answered by the serving controller, on its own connection, not by a command")
|
||||
case "root-free":
|
||||
return nil, errors.New("root-free is judged by the serving controller, on its own connection, not by a command")
|
||||
case "status":
|
||||
return []string{"status", "--json"}, nil
|
||||
case "nodes":
|
||||
@@ -1062,6 +1064,9 @@ func seatToolHandlers() (map[string]link.ToolHandler, []string, error) {
|
||||
if verb == "dead-letters" {
|
||||
return deadLettersAnswer(ctx, a)
|
||||
}
|
||||
if verb == "root-free" {
|
||||
return rootFreeAnswer(ctx, a.given["machines"], rootClock())
|
||||
}
|
||||
if verb == "doctor" {
|
||||
// From the serving controller, which runs the self-check and hears the signals
|
||||
// (novox/hq to-be 45 §4): the last verdict at once, or a run now.
|
||||
@@ -1152,7 +1157,10 @@ func actsOnAPlan(args map[string]any) bool {
|
||||
var inProcess = map[string]bool{"tools": true, "calls": true, "doctor": true,
|
||||
// What a consumer gave up on, read and changed on the serving controller's own connection (novox/hq
|
||||
// issue 330).
|
||||
"dead-letters": true}
|
||||
"dead-letters": true,
|
||||
// Whether a machine is root-free, judged live on the serving controller's store and connection (novox/hq ADR
|
||||
// 0259 §8): the router asks it before an approval.
|
||||
"root-free": true}
|
||||
|
||||
// answersFirst is a command line whose caller is answered before it runs: a push, by its verb or
|
||||
// through `command`. A push sends the machine holding the bus first when its user list changed, the
|
||||
|
||||
@@ -2,6 +2,7 @@ package broker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@@ -167,12 +168,25 @@ func PlacementsOf(r Records, interchangeable map[string]bool) Placements {
|
||||
for _, nodes := range p.Nodes {
|
||||
sort.Strings(nodes)
|
||||
}
|
||||
// Where the router runs: a verified sender is believed only while its machine is root-free too. A router
|
||||
// placed nowhere, or on more than one machine, frees nothing.
|
||||
var routerNodes []string
|
||||
for node, declared := range r.Assigned {
|
||||
for _, d := range declared {
|
||||
for _, s := range d.Holds {
|
||||
if s.Name == routerSeat && !slices.Contains(routerNodes, node) {
|
||||
routerNodes = append(routerNodes, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
routerFree := len(routerNodes) == 1 && r.RootFree[routerNodes[0]]
|
||||
for node, declared := range r.Assigned {
|
||||
for _, d := range declared {
|
||||
for _, s := range d.Holds {
|
||||
if s.Kinded && s.Kind != "" {
|
||||
p.Kinds = append(p.Kinds, KindHeld{Seat: s.Name, Kind: s.Kind, Module: d.Module, Node: node,
|
||||
Capabilities: placedCapabilities(s.Capabilities, d.RunsAs)})
|
||||
Capabilities: placedCapabilities(s.Capabilities, d.RunsAs, routerFree && r.RootFree[node])})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,13 +204,19 @@ func PlacementsOf(r Records, interchangeable map[string]bool) Placements {
|
||||
return p
|
||||
}
|
||||
|
||||
// routerSeat is the seat the router of asks holds (novox/hq ADR 0259 §3).
|
||||
const routerSeat = "operator-channel"
|
||||
|
||||
// placedCapabilities is what a kind's claim promises, as far as its placement lets the router believe it
|
||||
// (novox/hq ADR 0259 §8): `verified-sender` only from a holder that runs as an account of its own, on a bus
|
||||
// account of its own — never one the machine's runtime carries as the operator's account.
|
||||
func placedCapabilities(declared []string, runsAs string) []string {
|
||||
// account of its own — never one the machine's runtime carries as the operator's account — and only while
|
||||
// its machine and the router's were root-free when composed (rootFree; the review of 2026-10-09, H3). The
|
||||
// membership carrying it is composed at a push, so it can outlive a pass that later fails: the router asks
|
||||
// the controller's root-free verb again before it honours an approval, and that is the check that holds.
|
||||
func placedCapabilities(declared []string, runsAs string, rootFree bool) []string {
|
||||
var out []string
|
||||
for _, c := range declared {
|
||||
if c == "verified-sender" && runsAs == "" {
|
||||
if c == "verified-sender" && (runsAs == "" || !rootFree) {
|
||||
continue
|
||||
}
|
||||
out = append(out, c)
|
||||
|
||||
@@ -251,7 +251,7 @@ func TestTheRoutersMembershipNamesEveryKindAndItsCapabilities(t *testing.T) {
|
||||
records := Records{Nodes: []string{"anchor", "laptop"}, Assigned: map[string][]Declared{
|
||||
"anchor": {router, {Module: "telegram", Holds: []Seat{tg}, RunsAs: "telegram"}},
|
||||
"laptop": {{Module: "desk-channel", Holds: []Seat{desk}}},
|
||||
}}
|
||||
}, RootFree: map[string]bool{"anchor": true}}
|
||||
where := PlacementsOf(records, nil)
|
||||
m := MembershipFor("anchor", router, where)
|
||||
if m.SeatTraffic == nil || len(m.SeatTraffic.Kinds) != 2 {
|
||||
@@ -329,12 +329,62 @@ func TestTheMachinesRuntimeNeverCarriesATrustedHolder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0259 §8: verified-sender reaches the router only from a holder of its own account.
|
||||
// novox/hq ADR 0259 §8: verified-sender reaches the router only from a holder of its own account, on a machine
|
||||
// root-free when composed, with the router's own machine root-free too (the review of 2026-10-09, H3).
|
||||
func TestVerifiedSenderIsBelievedOnlyFromAHolderOfItsOwnAccount(t *testing.T) {
|
||||
if got := placedCapabilities([]string{"choice", "verified-sender"}, ""); namesVerb(got, "verified-sender") {
|
||||
if got := placedCapabilities([]string{"choice", "verified-sender"}, "", true); namesVerb(got, "verified-sender") {
|
||||
t.Errorf("a carried holder keeps verified-sender: %v", got)
|
||||
}
|
||||
if got := placedCapabilities([]string{"choice", "verified-sender"}, "telegram"); !namesVerb(got, "verified-sender") {
|
||||
t.Errorf("a holder of its own account lost verified-sender: %v", got)
|
||||
if got := placedCapabilities([]string{"choice", "verified-sender"}, "telegram", true); !namesVerb(got, "verified-sender") {
|
||||
t.Errorf("a holder of its own account on a root-free machine lost verified-sender: %v", got)
|
||||
}
|
||||
if got := placedCapabilities([]string{"choice", "verified-sender"}, "telegram", false); namesVerb(got, "verified-sender") ||
|
||||
!namesVerb(got, "choice") {
|
||||
t.Errorf("a holder on a machine not root-free keeps verified-sender, or lost the rest: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The kinds the router is told carry verified-sender only while the channel's machine and the router's are
|
||||
// both root-free as composed; no record of a pass is no pass, and neither is a router placed nowhere.
|
||||
func TestVerifiedSenderNeedsTheChannelsAndTheRoutersMachinesRootFree(t *testing.T) {
|
||||
tg := channelSeat("telegram")
|
||||
tg.Capabilities = []string{"choice", "verified-sender"}
|
||||
router := Declared{Module: "messenger", Holds: []Seat{operatorChannel()}, RunsAs: "messenger"}
|
||||
telegram := Declared{Module: "telegram", Holds: []Seat{tg}, RunsAs: "telegram"}
|
||||
verified := func(r Records) bool {
|
||||
for _, k := range PlacementsOf(r, nil).Kinds {
|
||||
if k.Kind == "telegram" {
|
||||
return namesVerb(k.Capabilities, "verified-sender")
|
||||
}
|
||||
}
|
||||
t.Fatal("telegram not placed")
|
||||
return false
|
||||
}
|
||||
same := func(free map[string]bool) Records {
|
||||
return Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {router, telegram}}, RootFree: free}
|
||||
}
|
||||
apart := func(free map[string]bool) Records {
|
||||
return Records{Nodes: []string{"anchor", "relay"},
|
||||
Assigned: map[string][]Declared{"anchor": {router}, "relay": {telegram}}, RootFree: free}
|
||||
}
|
||||
if !verified(same(map[string]bool{"anchor": true})) {
|
||||
t.Error("both on one root-free machine: verified-sender withheld")
|
||||
}
|
||||
if verified(same(nil)) {
|
||||
t.Error("no record of a pass, and verified-sender kept")
|
||||
}
|
||||
if verified(apart(map[string]bool{"relay": true})) {
|
||||
t.Error("the router's machine not root-free, and verified-sender kept")
|
||||
}
|
||||
if verified(apart(map[string]bool{"anchor": true})) {
|
||||
t.Error("the channel's machine not root-free, and verified-sender kept")
|
||||
}
|
||||
if !verified(apart(map[string]bool{"anchor": true, "relay": true})) {
|
||||
t.Error("both machines root-free: verified-sender withheld")
|
||||
}
|
||||
noRouter := Records{Nodes: []string{"relay"}, Assigned: map[string][]Declared{"relay": {telegram}},
|
||||
RootFree: map[string]bool{"relay": true}}
|
||||
if verified(noRouter) {
|
||||
t.Error("no router placed, and verified-sender kept")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ type Records struct {
|
||||
// Interchangeable is each module whose definition says its instances are the same anywhere
|
||||
// (ADR 0160), which decides whether the module's plain subject is issued to every instance.
|
||||
Interchangeable map[string]bool
|
||||
// RootFree is each machine judged root-free when this was composed (novox/hq ADR 0259 §8): it names an
|
||||
// account agents run as, judged unable to become root by its node-engine, and serves no login shell
|
||||
// execute. A machine absent is not free: no record of a pass is no pass.
|
||||
RootFree map[string]bool
|
||||
}
|
||||
|
||||
// Users is every user the composed file should contain, in the order it will be written.
|
||||
|
||||
@@ -429,6 +429,15 @@ var ControllerVerbs = []Verb{
|
||||
"cause": "with consumer or older-than: the cause in a word (cleanup-waiting when absent)",
|
||||
}, nil, "confirm")},
|
||||
// What a consumer gave up on (novox/hq issue 330): kept in DEAD_LETTERS until a person acts on it.
|
||||
{Name: "root-free", Description: "Whether each machine named is root-free now (novox/hq ADR 0259 §8): no agent " +
|
||||
"there can become root without a person. Judged when asked, never from a condition: free only when the machine " +
|
||||
"names an account its agents run as, its node-engine judged that account unable to become root within the " +
|
||||
"last 15 minutes, and the login shell's execute is not served there. Anything else, a read that failed " +
|
||||
"included, is not free and says why. The router asks it before an answer from a channel proving its sender " +
|
||||
"may approve. Only reads.",
|
||||
Input: schema(map[string]string{
|
||||
"machines": "the machines to judge, separated by commas",
|
||||
}, []string{"machines"})},
|
||||
{Name: "dead-letters", Description: "Every message a consumer on the bus gave up on after handing it over " +
|
||||
"as often as it may, kept in DEAD_LETTERS: whose consumer, the subject, how often it was handed over and " +
|
||||
"when it was given up, newest first. With id: that one whole, with what it said. With deliver: hand it " +
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"retire",
|
||||
"cleanup",
|
||||
"dead-letters",
|
||||
"root-free",
|
||||
"data",
|
||||
"build",
|
||||
"artifacts",
|
||||
|
||||
Reference in New Issue
Block a user