Serve a trusted holder from a runtime of its own account, refuse it in the machine's runtime, and say while an agent can become root where it runs (hq ADR 0259 §8)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery ready: it delivers once merged
mesh/delivery-group group feat/asks-answered-on-any-channel rejected: a member's own check failed
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery ready: it delivers once merged
mesh/delivery-group group feat/asks-answered-on-any-channel rejected: a member's own check failed
This commit is contained in:
@@ -60,14 +60,14 @@ func assertBusObjects(ctx context.Context, inv *inventory.Inventory, r broker.Ra
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range trafficStreams {
|
||||
if err := r.EnsureStream(s); err != nil {
|
||||
return nil, fmt.Errorf("asserting the work queue %s: %w", s.Name, err)
|
||||
}
|
||||
}
|
||||
// Every one tried, and every failure named: one module's consumer the bus refuses is no reason
|
||||
// the modules after it in the list hear nothing (novox/hq issue 208, where this runs on each send).
|
||||
var failed []error
|
||||
for _, s := range trafficStreams {
|
||||
if err := r.EnsureStream(s); err != nil {
|
||||
failed = append(failed, fmt.Errorf("the work queue %s: %w", s.Name, err))
|
||||
}
|
||||
}
|
||||
for _, c := range trafficWorkers {
|
||||
if err := r.EnsureConsumer(c); err != nil {
|
||||
failed = append(failed, fmt.Errorf("the worker %s on %s: %w", c.Name, c.Stream, err))
|
||||
@@ -159,6 +159,22 @@ func seatTrafficObjects(ctx context.Context, inv *inventory.Inventory) ([]broker
|
||||
return nil, nil, err
|
||||
}
|
||||
streams, workers := broker.SeatTrafficObjects(users)
|
||||
// And the queue of every such seat the catalogue declares, held or not: work queues from registration,
|
||||
// so what is submitted before a holder is assigned waits for it (the correctness review of 2026-10-08).
|
||||
declared, err := inv.DeclaredTrafficSeats(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
have := map[string]bool{}
|
||||
for _, s := range streams {
|
||||
have[s.Name] = true
|
||||
}
|
||||
for _, s := range broker.TrafficQueues(declared) {
|
||||
if !have[s.Name] {
|
||||
streams = append(streams, s)
|
||||
have[s.Name] = true
|
||||
}
|
||||
}
|
||||
return streams, workers, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,11 @@ var probeRegistry = []probe{
|
||||
{ID: probeDeliveriesID, Asserts: "no delivery is held past its state's bound unsaid: mesh-delivery's " +
|
||||
"`stalled`, each with the transition its table lets healer H2 take", From: "ADR 0239",
|
||||
Kind: kindDeliveryStalled, Phase: 3, run: probeDeliveries},
|
||||
// Root where the trusted parties run (novox/hq ADR 0259 §8): while an agent can become root there without a
|
||||
// person, an answer proven there proves nothing.
|
||||
{ID: "D-root", Asserts: "no agent can become root without a person on a machine where the router or a channel " +
|
||||
"proving its sender runs: not by its own account, and not through a tool that runs its command as an account " +
|
||||
"that can", From: "ADR 0259 §8", Kind: kindAgentRoot, Phase: 2, run: probeAgentRoot},
|
||||
{ID: "DW", Asserts: "the watchdogs of the signals table ran within three of their intervals",
|
||||
From: "ADR 0227 rule 6: the watchers are watched", Kind: "watchdogs-silent", Phase: 1, run: probeWatchdogs},
|
||||
// The core's health definitions (novox/hq to-be 45 §8, ADR 0236): what a core component's new build is
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// The self-check's probe of who can become root where the trusted parties run (novox/hq ADR 0259 §8, rule 3).
|
||||
//
|
||||
// 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:
|
||||
//
|
||||
// 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?
|
||||
//
|
||||
// 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".
|
||||
|
||||
// kindAgentRoot is the condition an agent able to become root where a trusted party runs raises.
|
||||
const kindAgentRoot = "agent-root"
|
||||
|
||||
// The tools the probe asks, of the modules on each machine.
|
||||
const (
|
||||
agentModule = "claude-code"
|
||||
agentStatusTool = "claude_code_status"
|
||||
sudoModule = "sudo"
|
||||
sudoEscalation = "sudo_escalation"
|
||||
loginShellSeat = "node-login-shell"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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 seat is held there, running commands as the runtime's account
|
||||
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 {
|
||||
ways = append(ways, fmt.Sprintf("the login shell runs any command an agent gives it as %s, which can "+
|
||||
"become root without a person: %s", f.Runtime, 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{}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.Manifest.ClaimsSeat(loginShellSeat) {
|
||||
for _, node := range e.On {
|
||||
if f := facts[node]; f != nil {
|
||||
f.LoginShell = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
machines := make([]string, 0, len(facts))
|
||||
for m := range facts {
|
||||
machines = append(machines, m)
|
||||
}
|
||||
sort.Strings(machines)
|
||||
var out []conditions.Observation
|
||||
var unread []string
|
||||
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)...)
|
||||
}
|
||||
if len(unread) > 0 {
|
||||
return out, fmt.Errorf("who can become root could not be measured: %s", strings.Join(unread, "; "))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
)
|
||||
|
||||
// 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{}}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,12 @@ func handOver(ctx context.Context, seatName, to string, adding bool) error {
|
||||
if !ok || nodeName == "" || module == "" {
|
||||
return fmt.Errorf("the new holder is named <node>/<module>, not %q", to)
|
||||
}
|
||||
// A kinded bench is held once per kind, by the claims themselves (novox/hq ADR 0234 §2, ADR 0259): the
|
||||
// record of who holds a seat has no kind, so a handover would name one holder for every kind.
|
||||
if catalogue.KindedBenches[seatName] {
|
||||
return fmt.Errorf("%s is a kinded bench: each kind is held by the module claiming it, and is not handed "+
|
||||
"over by `seat` — assign the module that claims the kind, or unassign the one that does", seatName)
|
||||
}
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
@@ -62,3 +64,13 @@ func TestAClaimOutsideTheSetIsShownNotHidden(t *testing.T) {
|
||||
t.Fatalf("a claim outside the set was not shown: %+v", outside)
|
||||
}
|
||||
}
|
||||
|
||||
// A kinded bench is not handed over by `seat`: each kind is held by its claim (novox/hq ADR 0259).
|
||||
func TestAKindedBenchIsNotHandedOver(t *testing.T) {
|
||||
for _, bench := range []string{"channel", "intake"} {
|
||||
err := handOver(context.Background(), bench, "anchor/telegram", false)
|
||||
if err == nil || !strings.Contains(err.Error(), "is a kinded bench") {
|
||||
t.Errorf("%s: %v", bench, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user